From cebb742224a44758c8be70e431206847496d126b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 06:50:44 +0200 Subject: [PATCH 1/3] fix: finish session captures through lifetime bindings --- .../audio-probe-session-resource.test.ts | 4 +- .../durable-capture-resource.fixtures.ts | 11 +-- .../durable-capture-resource.test.ts | 4 +- .../screen-recording-boundary-faults.test.ts | 7 +- .../screen-recording-session-resource.test.ts | 12 +-- .../audio-probe-session-resource.ts | 17 +--- .../durable-capture-resource.ts | 33 +++----- .../perf-capture-session-resource.ts | 17 +--- .../screen-recording-session-resource.ts | 13 +--- .../screen-recording-stop-recovery.ts | 8 +- .../src/durable-capture/adoption.ts | 4 +- .../src/durable-capture/definition.ts | 19 ----- .../durable-capture.fixtures.ts | 22 ++---- .../session-binding.fixtures.ts | 7 +- .../src/durable-capture/transitions.test.ts | 24 ++---- .../src/durable-capture/transitions.ts | 42 +++------- .../src/recording/failed-finish.test.ts | 40 ++++------ .../app-log-session-resource.test.ts | 64 ++++++++++++++- .../__tests__/daemon-runtime-app-log.test.ts | 2 +- .../perf-capture-session-resource.test.ts | 4 +- src/daemon/app-log-session-resource.ts | 26 +++---- src/daemon/handlers/record-runtime.ts | 22 ++++-- src/daemon/request-execution-scope.ts | 5 +- .../daemon-runtime-device-claims.test.ts | 4 +- .../daemon-runtime-recording-teardown.test.ts | 4 +- .../daemon-runtime-web-close-teardown.test.ts | 6 +- src/daemon/server/daemon-runtime.ts | 29 +++---- .../session-teardown-resources.test.ts | 37 +++++---- .../session-close-lifecycle-teardown.ts | 20 ++--- .../internal/session-audio.ts | 31 +++----- .../internal/session-observability.ts | 8 +- .../internal/session-perf-runtime.ts | 11 +-- src/daemon/session-teardown.ts | 78 +++++++------------ 33 files changed, 258 insertions(+), 377 deletions(-) diff --git a/packages/capture-kit/src/capture-admission/__tests__/audio-probe-session-resource.test.ts b/packages/capture-kit/src/capture-admission/__tests__/audio-probe-session-resource.test.ts index 359fb5f545..c0e4d38565 100644 --- a/packages/capture-kit/src/capture-admission/__tests__/audio-probe-session-resource.test.ts +++ b/packages/capture-kit/src/capture-admission/__tests__/audio-probe-session-resource.test.ts @@ -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'); diff --git a/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.fixtures.ts b/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.fixtures.ts index e022419770..291cd0d543 100644 --- a/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.fixtures.ts +++ b/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.fixtures.ts @@ -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, diff --git a/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.test.ts b/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.test.ts index a77610fac5..6a1e6e7a15 100644 --- a/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.test.ts +++ b/packages/capture-kit/src/capture-admission/__tests__/durable-capture-resource.test.ts @@ -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(); diff --git a/packages/capture-kit/src/capture-admission/__tests__/screen-recording-boundary-faults.test.ts b/packages/capture-kit/src/capture-admission/__tests__/screen-recording-boundary-faults.test.ts index dfab3f92e8..e13d95baff 100644 --- a/packages/capture-kit/src/capture-admission/__tests__/screen-recording-boundary-faults.test.ts +++ b/packages/capture-kit/src/capture-admission/__tests__/screen-recording-boundary-faults.test.ts @@ -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, diff --git a/packages/capture-kit/src/capture-admission/__tests__/screen-recording-session-resource.test.ts b/packages/capture-kit/src/capture-admission/__tests__/screen-recording-session-resource.test.ts index a64497c19d..99e3b36278 100644 --- a/packages/capture-kit/src/capture-admission/__tests__/screen-recording-session-resource.test.ts +++ b/packages/capture-kit/src/capture-admission/__tests__/screen-recording-session-resource.test.ts @@ -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', }); @@ -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); @@ -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, }); }; diff --git a/packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts b/packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts index ddcd8037b0..21f2b91f1a 100644 --- a/packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts +++ b/packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts @@ -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, @@ -63,9 +54,7 @@ export function adoptStartedAudioProbe(params: { } export function finishLiveAudioProbe(params: { - session: DurableCaptureSessionState; - sessionName: string; - sessionStore: DurableCaptureSessionStore; + binding: DurableCaptureSessionBinding<'audio-probe', AudioProbeLiveHandle>; intent: DurableCaptureFinishIntent; }): Promise { return audioProbeDurableResource.finishLive(params); diff --git a/packages/capture-kit/src/capture-admission/durable-capture-resource.ts b/packages/capture-kit/src/capture-admission/durable-capture-resource.ts index 4dd4636244..8a9800e69c 100644 --- a/packages/capture-kit/src/capture-admission/durable-capture-resource.ts +++ b/packages/capture-kit/src/capture-admission/durable-capture-resource.ts @@ -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'; @@ -34,19 +34,12 @@ type SessionCaptureRecoveryParams; -/** - * 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, - S, ->(definition: DurableCaptureResourceDefinition) { +>(definition: DurableCaptureRecordDefinition) { const sessionResourcePath = ( sessionStore: Readonly<{ resolveSessionDir(name: string): string }>, sessionName: string, @@ -84,27 +77,23 @@ export function createDurableCaptureResource< ); }, finishLive(params: { - session: S; - sessionName: string; - sessionStore: DurableCaptureSessionStore; + binding: DurableCaptureSessionBinding; intent: DurableCaptureFinishIntent; }): Promise { return finishLiveDurableCapture( definition, params, - sessionResourcePath(params.sessionStore, params.sessionName), + definition.store.resolvePath(params.binding.sessionDir), ); }, finishRecovered(params: FinishRecoveredDurableCaptureParams): Promise { return finishRecoveredDurableCapture(definition, params); }, - forceCleanupLive(params: { - session: S; - sessionName?: string; - sessionStore?: DurableCaptureSessionStore; - resourcePath: string; - }): Promise { - return forceCleanupLiveDurableCapture(definition, params); + forceCleanupLive(params: { binding: DurableCaptureSessionBinding }): Promise { + return forceCleanupLiveDurableCapture(definition, { + ...params, + resourcePath: definition.store.resolvePath(params.binding.sessionDir), + }); }, recoverAll(params: SessionCaptureRecoveryParams) { return recoverDurableCaptureResourcesAfterDaemonLock(recoveryParams(params)); diff --git a/packages/capture-kit/src/capture-admission/perf-capture-session-resource.ts b/packages/capture-kit/src/capture-admission/perf-capture-session-resource.ts index 41546e034f..c6dbee2515 100644 --- a/packages/capture-kit/src/capture-admission/perf-capture-session-resource.ts +++ b/packages/capture-kit/src/capture-admission/perf-capture-session-resource.ts @@ -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', @@ -61,9 +52,7 @@ export function adoptStartedPerfCapture(params: { } export function finishLivePerfCapture(params: { - session: DurableCaptureSessionState; - sessionName: string; - sessionStore: DurableCaptureSessionStore; + binding: DurableCaptureSessionBinding<'perf-capture', PerfNativeCaptureLiveHandle>; intent: DurableCaptureFinishIntent; }): Promise { return perfCaptureDurableResource.finishLive(params); diff --git a/packages/capture-kit/src/capture-admission/screen-recording-session-resource.ts b/packages/capture-kit/src/capture-admission/screen-recording-session-resource.ts index 2286e814e9..9a477dfbaf 100644 --- a/packages/capture-kit/src/capture-admission/screen-recording-session-resource.ts +++ b/packages/capture-kit/src/capture-admission/screen-recording-session-resource.ts @@ -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. @@ -63,9 +56,7 @@ export function adoptStartedScreenRecording(params: { } export function finishLiveScreenRecording(params: { - session: DurableCaptureSessionState; - sessionName: string; - sessionStore: DurableCaptureSessionStore; + binding: DurableCaptureSessionBinding<'screen-recording', ScreenRecordingLiveHandle>; intent: DurableCaptureFinishIntent; }): Promise { return screenRecordingDurableResource.finishLive(params); diff --git a/packages/capture-kit/src/capture-admission/screen-recording-stop-recovery.ts b/packages/capture-kit/src/capture-admission/screen-recording-stop-recovery.ts index b59ee12d04..585a292243 100644 --- a/packages/capture-kit/src/capture-admission/screen-recording-stop-recovery.ts +++ b/packages/capture-kit/src/capture-admission/screen-recording-stop-recovery.ts @@ -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. @@ -45,7 +41,7 @@ const OPTIONAL_RESPONSE_FIELDS = [ type ScreenRecordingManifestParams = Readonly<{ sessionName: string; - sessionStore: DurableCaptureSessionStore; + sessionStore: Readonly<{ resolveSessionDir(name: string): string }>; }>; export function resolveScreenRecordingStopRecovery( diff --git a/packages/capture-kit/src/durable-capture/adoption.ts b/packages/capture-kit/src/durable-capture/adoption.ts index eba37b023e..fef4f1661a 100644 --- a/packages/capture-kit/src/durable-capture/adoption.ts +++ b/packages/capture-kit/src/durable-capture/adoption.ts @@ -15,7 +15,6 @@ import { import type { AdoptStartedDurableCaptureParams, DurableCaptureRecordDefinition, - DurableCaptureResourceDefinition, } from './definition.ts'; import { capitalizeDurableCaptureLabel, durableCaptureDiagnosticPrefix } from './labels.ts'; @@ -28,9 +27,8 @@ export async function adoptStartedDurableCapture< K extends string, H extends LiveResourceHandle, C, - S, >( - definition: DurableCaptureResourceDefinition, + definition: DurableCaptureRecordDefinition, params: AdoptStartedDurableCaptureParams, resourcePath: string, ): Promise { diff --git a/packages/capture-kit/src/durable-capture/definition.ts b/packages/capture-kit/src/durable-capture/definition.ts index 4b0aba2fdb..0b947fdae9 100644 --- a/packages/capture-kit/src/durable-capture/definition.ts +++ b/packages/capture-kit/src/durable-capture/definition.ts @@ -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, @@ -24,16 +23,6 @@ export type DurableCaptureSessionBinding): 'cleared' | 'retired' | 'resource-changed'; }>; -export type DurableCaptureSessionStore = Readonly<{ - set(name: string, session: S): void; - resolveSessionDir(name: string): string; -}>; - -export type DurableCaptureSessionSlot = Readonly<{ - read(session: S): DurableCaptureSessionResource | undefined; - replace(session: S, resource: DurableCaptureSessionResource | 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 @@ -75,14 +64,6 @@ export type DurableCaptureRecordDefinition = Readonly<{ }>; }>; -export type DurableCaptureResourceDefinition< - K extends string, - H extends LiveResourceHandle, - C, - S, -> = DurableCaptureRecordDefinition & - Readonly<{ sessionSlot: DurableCaptureSessionSlot }>; - /** * 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. diff --git a/packages/capture-kit/src/durable-capture/durable-capture.fixtures.ts b/packages/capture-kit/src/durable-capture/durable-capture.fixtures.ts index fcb88e857c..363b83a45c 100644 --- a/packages/capture-kit/src/durable-capture/durable-capture.fixtures.ts +++ b/packages/capture-kit/src/durable-capture/durable-capture.fixtures.ts @@ -14,7 +14,7 @@ import { mkdtempForTestSync } from '../tmp-dir.fixtures.ts'; import type { DurableCaptureCleanupOutcome, DurableCaptureFailedFinishPolicy, - DurableCaptureResourceDefinition, + DurableCaptureRecordDefinition, DurableCaptureSessionResource, } from './definition.ts'; import { createDurableCaptureResourceStore, type DurableCaptureResourceStore } from './store.ts'; @@ -49,21 +49,12 @@ export const testCaptureStore = createDurableCaptureResourceStore({ export function createTestCaptureDefinition( store: DurableCaptureResourceStore = testCaptureStore, failedFinishPolicy: DurableCaptureFailedFinishPolicy = 'dispose-on-failed-finish', -): DurableCaptureResourceDefinition< - typeof TEST_CAPTURE_KIND, - TestCaptureHandle, - TestCaptureCompletion, - TestCaptureSession -> { +): DurableCaptureRecordDefinition { return { resourceKind: TEST_CAPTURE_KIND, displayName: 'test capture', store, failedFinishPolicy, - sessionSlot: { - read: (session) => session.capture, - replace: (session, capture) => ({ ...session, capture }), - }, completionMetadata: (completion) => ({ outputPath: completion.outputPath, completedAt: completion.completedAt, @@ -101,11 +92,10 @@ export function makeDurableCaptureContext( > = vi.fn(); return { reportUndurableCleanup, - binding: makeCaptureSessionBinding( - sessionStore, - sessionName, - testCaptureDefinition.sessionSlot, - ), + binding: makeCaptureSessionBinding(sessionStore, sessionName, { + read: (session) => session.capture, + replace: (session, capture) => ({ ...session, capture }), + }), sessions, sessionsDir, resolveSessionDir, diff --git a/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts b/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts index 611d9b9108..8c37b7be49 100644 --- a/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts +++ b/packages/capture-kit/src/durable-capture/session-binding.fixtures.ts @@ -1,5 +1,5 @@ import { AppError } from '@agent-device/kernel/errors'; -import type { DurableCaptureSessionBinding, DurableCaptureSessionSlot } from './definition.ts'; +import type { DurableCaptureSessionBinding, DurableCaptureSessionResource } from './definition.ts'; export function makeCaptureSessionBinding( store: Readonly<{ @@ -8,7 +8,10 @@ export function makeCaptureSessionBinding, address: string, - slot: DurableCaptureSessionSlot, + slot: Readonly<{ + read(session: S): DurableCaptureSessionResource | undefined; + replace(session: S, resource: DurableCaptureSessionResource | undefined): S; + }>, ): DurableCaptureSessionBinding { const requireSession = (): S => { const session = store.get(address); diff --git a/packages/capture-kit/src/durable-capture/transitions.test.ts b/packages/capture-kit/src/durable-capture/transitions.test.ts index 5085197ca1..f691674c53 100644 --- a/packages/capture-kit/src/durable-capture/transitions.test.ts +++ b/packages/capture-kit/src/durable-capture/transitions.test.ts @@ -39,9 +39,7 @@ test('finish failure remains primary when cleanup and cleanup-pending persistenc finishLiveDurableCapture( definition, { - session: active, - sessionName: context.sessionName, - sessionStore: context.sessionStore, + binding: context.binding, intent: 'capture', }, context.resourcePath, @@ -70,9 +68,7 @@ test('an uncertain finish preserves its error after confirmed compensating clean finishLiveDurableCapture( testCaptureDefinition, { - session: active, - sessionName: context.sessionName, - sessionStore: context.sessionStore, + binding: context.binding, intent: 'capture', }, context.resourcePath, @@ -106,9 +102,7 @@ test('an uncertain finish retains live evidence when compensating cleanup is unc finishLiveDurableCapture( testCaptureDefinition, { - session: active, - sessionName: context.sessionName, - sessionStore: context.sessionStore, + binding: context.binding, intent: 'capture', }, context.resourcePath, @@ -145,9 +139,7 @@ test('a preserved finish leaves the record open without disposing what its retry finishLiveDurableCapture( definition, { - session: active, - sessionName: context.sessionName, - sessionStore: context.sessionStore, + binding: context.binding, intent: 'capture', }, context.resourcePath, @@ -181,9 +173,7 @@ test('a preserved finish that reports uncertainty still leaves the record retrya finishLiveDurableCapture( definition, { - session: active, - sessionName: context.sessionName, - sessionStore: context.sessionStore, + binding: context.binding, intent: 'capture', }, context.resourcePath, @@ -216,9 +206,7 @@ test('a disposal finish disposes a preserving kind’s material too', async () = finishLiveDurableCapture( definition, { - session: active, - sessionName: context.sessionName, - sessionStore: context.sessionStore, + binding: context.binding, intent: 'disposal', }, context.resourcePath, diff --git a/packages/capture-kit/src/durable-capture/transitions.ts b/packages/capture-kit/src/durable-capture/transitions.ts index 8e3a2590e2..b6de89d1fb 100644 --- a/packages/capture-kit/src/durable-capture/transitions.ts +++ b/packages/capture-kit/src/durable-capture/transitions.ts @@ -12,8 +12,7 @@ import { withDurableCaptureResourceFence, type DurableCaptureResourceFenceLease import type { DurableCaptureFinishIntent, DurableCaptureRecordDefinition, - DurableCaptureResourceDefinition, - DurableCaptureSessionStore, + DurableCaptureSessionBinding, } from './definition.ts'; import { capitalizeDurableCaptureLabel, durableCaptureDiagnosticPrefix } from './labels.ts'; @@ -21,18 +20,15 @@ export async function finishLiveDurableCapture< K extends string, H extends LiveResourceHandle, C, - S, >( - definition: DurableCaptureResourceDefinition, + definition: DurableCaptureRecordDefinition, params: { - session: S; - sessionName: string; - sessionStore: DurableCaptureSessionStore; + binding: DurableCaptureSessionBinding; intent: DurableCaptureFinishIntent; }, resourcePath: string, ): Promise { - const active = definition.sessionSlot.read(params.session); + const active = params.binding.read(); if (!active) throw new AppError('INVALID_ARGS', definition.messages.noActive); try { const result = await finishDurableCaptureHandle(definition, { @@ -41,12 +37,12 @@ export async function finishLiveDurableCapture< resourcePath, intent: params.intent, }); - clearLiveSlot(definition, params); + params.binding.clear(active); return result; } catch (error) { const record = definition.store.read(resourcePath); if (record.status === 'decoded' && record.envelope.lifecycle === 'completed') { - clearLiveSlot(definition, params); + params.binding.clear(active); } throw error; } @@ -186,16 +182,6 @@ function emitFailedFinishCleanupDiagnostic( }); } -function clearLiveSlot, C, S>( - definition: DurableCaptureResourceDefinition, - params: { session: S; sessionName: string; sessionStore: DurableCaptureSessionStore }, -): void { - params.sessionStore.set( - params.sessionName, - definition.sessionSlot.replace(params.session, undefined), - ); -} - function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } @@ -204,17 +190,14 @@ export async function forceCleanupLiveDurableCapture< K extends string, H extends LiveResourceHandle, C, - S, >( - definition: DurableCaptureResourceDefinition, + definition: DurableCaptureRecordDefinition, params: { - session: S; - sessionName?: string; - sessionStore?: DurableCaptureSessionStore; + binding: DurableCaptureSessionBinding; resourcePath: string; }, ): Promise { - const active = definition.sessionSlot.read(params.session); + const active = params.binding.read(); if (!active) return; const outcome = await withDurableCaptureResourceFence({ store: definition.store, @@ -228,12 +211,7 @@ export async function forceCleanupLiveDurableCapture< }, }); requireConfirmedDurableCaptureCleanup(definition, outcome); - if (params.sessionStore && params.sessionName) { - params.sessionStore.set( - params.sessionName, - definition.sessionSlot.replace(params.session, undefined), - ); - } + params.binding.clear(active); } export function transitionCleanupOutcome( diff --git a/packages/platform-android/src/recording/failed-finish.test.ts b/packages/platform-android/src/recording/failed-finish.test.ts index d3f7afb0aa..38f9be8955 100644 --- a/packages/platform-android/src/recording/failed-finish.test.ts +++ b/packages/platform-android/src/recording/failed-finish.test.ts @@ -12,7 +12,7 @@ import { adoptStartedDurableCapture, createDurableCaptureResourceStore, finishLiveDurableCapture, - type DurableCaptureResourceDefinition, + type DurableCaptureRecordDefinition, } from '@agent-device/capture-kit/durable-capture'; import { mkdtempForTestSync } from '../__tests__/test-utils/tmp-dir.ts'; import { androidRecordingDevice, recordingHost, recordingInput } from './fixtures.ts'; @@ -113,7 +113,7 @@ type AndroidRecordingSession = Readonly<{ /** * The daemon's recording record assembled around the real Android handle: the same definition the - * daemon declares in `src/daemon/screen-recording-session-resource.ts`, including its policy, + * daemon declares in `packages/capture-kit/src/capture-admission/screen-recording-session-resource.ts`, including its policy, * driving the shared coordinator. */ async function adoptAndroidRecording(params: { @@ -127,26 +127,18 @@ async function adoptAndroidRecording(params: { fileName: 'screen-recording.resource.json', displayName: 'screen recording', }); - const definition: DurableCaptureResourceDefinition< - 'screen-recording', - ScreenRecordingLiveHandle, - ScreenRecordingCompletion, - AndroidRecordingSession - > = { - resourceKind: 'screen-recording', - displayName: 'screen recording', - store, - failedFinishPolicy: 'preserve-retry-material', - sessionSlot: { - read: (session) => session.recording, - replace: (session, recording) => ({ ...session, recording }), - }, - completionMetadata: (completion) => ({ outPath: completion.outPath }), - messages: { - noActive: 'no active recording', - cleanupPendingHint: 'Keep screen-recording.resource.json and retry stop.', - }, - }; + const definition: DurableCaptureRecordDefinition<'screen-recording', ScreenRecordingCompletion> = + { + resourceKind: 'screen-recording', + displayName: 'screen recording', + store, + failedFinishPolicy: 'preserve-retry-material', + completionMetadata: (completion) => ({ outPath: completion.outPath }), + messages: { + noActive: 'no active recording', + cleanupPendingHint: 'Keep screen-recording.resource.json and retry stop.', + }, + }; const sessionsDir = mkdtempForTestSync('agent-device-android-failed-finish-session-'); let session: AndroidRecordingSession = {}; const sessionStore = { @@ -193,9 +185,7 @@ async function adoptAndroidRecording(params: { finishLiveDurableCapture( definition, { - session, - sessionName: params.sessionName, - sessionStore, + binding, intent: 'capture', }, resourcePath, diff --git a/src/daemon/__tests__/app-log-session-resource.test.ts b/src/daemon/__tests__/app-log-session-resource.test.ts index eb9402d3ba..2a20ed6a0c 100644 --- a/src/daemon/__tests__/app-log-session-resource.test.ts +++ b/src/daemon/__tests__/app-log-session-resource.test.ts @@ -296,7 +296,6 @@ test('app-log disposes on a failed finish because its retry is that same finish finishSessionAppLog({ intent: 'capture', ...context, - session: context.sessionStore.get(context.sessionName) ?? context.session, }), ).rejects.toBe(finishError); @@ -386,6 +385,69 @@ test('late adoption disposes its pending handle without overwriting a successor }); }); +test.each(['rebuild', 'retire', 'replace-resource'] as const)( + 'finishing app log after %s preserves the current record and its other fields', + async (change) => { + const context = makeContext(); + const { + result: { envelope }, + } = makeStartResult(context); + let enter!: () => void; + let resume!: () => void; + const entered = new Promise((resolve) => { + enter = resolve; + }); + const release = new Promise((resolve) => { + resume = resolve; + }); + const finish = vi.fn(async () => { + enter(); + await release; + return { + status: 'completed' as const, + result: { backend: 'android' as const, outputPath: '/tmp/app.log', completedAt: 2 }, + }; + }); + const handle = createTestAppLogLiveHandle({ + inspect: () => ({ backend: 'android', state: 'active', startedAt: 1 }), + finish, + forceCleanup: async () => ({ status: 'cleaned' }), + }); + await adoptStartedSessionAppLog({ + ...context, + envelope, + pendingHandle: new PendingTransferGuard(handle), + throwIfCanceled: () => {}, + }); + const finishing = finishSessionAppLog({ ...context, intent: 'capture' }); + await entered; + let currentRef = context.ref; + const active = context.sessionStore.requireCurrent(currentRef).appLog!; + const replacementHandle = makeStartResult(context).handle; + if (change === 'retire') { + context.sessionStore.retire(currentRef); + currentRef = context.sessionStore.publish(context.sessionName, { + ...context.session, + appLog: active, + appName: 'successor', + }); + } else { + context.sessionStore.update(currentRef, { + appName: 'updated', + appLog: + change === 'replace-resource' ? { ...active, handle: replacementHandle } : { ...active }, + }); + } + resume(); + await finishing; + expect(finish).toHaveBeenCalledOnce(); + const current = context.sessionStore.requireCurrent(currentRef); + expect(current.appName).toBe(change === 'retire' ? 'successor' : 'updated'); + if (change === 'rebuild') expect(current.appLog).toBeUndefined(); + else expect(current.appLog?.handle).toBe(change === 'retire' ? handle : replacementHandle); + }, +); + function makeContext( device: DeviceInfo = { platform: 'android', diff --git a/src/daemon/__tests__/daemon-runtime-app-log.test.ts b/src/daemon/__tests__/daemon-runtime-app-log.test.ts index baad8d7a7a..6fee6e32a4 100644 --- a/src/daemon/__tests__/daemon-runtime-app-log.test.ts +++ b/src/daemon/__tests__/daemon-runtime-app-log.test.ts @@ -132,7 +132,7 @@ test('daemon shutdown settles fenced app-log cleanup before finalization can rel const beforeDelete = vi.fn(async () => {}); const teardown = teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stderr: { write: () => {} }, beforeDelete, diff --git a/src/daemon/__tests__/perf-capture-session-resource.test.ts b/src/daemon/__tests__/perf-capture-session-resource.test.ts index 6d7d4e8841..ecd14eb954 100644 --- a/src/daemon/__tests__/perf-capture-session-resource.test.ts +++ b/src/daemon/__tests__/perf-capture-session-resource.test.ts @@ -127,9 +127,7 @@ test('a perf stop whose pull failed re-collects the device-side trace the first const stop = () => finishLivePerfCapture({ intent: 'capture', - session: sessionStore.get(sessionName) ?? session, - sessionName, - sessionStore, + binding: bindSessionPerfCapture(sessionStore, sessionStore.lookup(sessionName)!), }); await expect(stop()).rejects.toBe(pullFailure); diff --git a/src/daemon/app-log-session-resource.ts b/src/daemon/app-log-session-resource.ts index 5c3993e19b..89335eebcb 100644 --- a/src/daemon/app-log-session-resource.ts +++ b/src/daemon/app-log-session-resource.ts @@ -31,16 +31,11 @@ export type AppLogSessionSnapshot = Readonly<{ export const appLogDurableResource = createDurableCaptureResource< 'app-log', AppLogLiveHandle, - AppLogCompletion, - SessionState + AppLogCompletion >({ resourceKind: 'app-log', displayName: 'app-log', store: appLogResourceStore, - sessionSlot: { - read: (session) => session.appLog, - replace: (session, appLog) => ({ ...session, appLog, appLogFailure: undefined }), - }, completionMetadata: (completion) => ({ backend: completion.backend, outputPath: completion.outputPath, @@ -93,22 +88,23 @@ export function adoptStartedSessionAppLog(params: { } export function finishSessionAppLog(params: { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; - resourcePath: string; intent: DurableCaptureFinishIntent; }): Promise { - return appLogDurableResource.finishLive(params); + return appLogDurableResource.finishLive({ + binding: bindSessionAppLog(params.sessionStore, params.ref), + intent: params.intent, + }); } export function forceCleanupSessionAppLog(params: { - session: SessionState; - sessionName?: string; - sessionStore?: SessionStore; - resourcePath: string; + ref: SessionRef; + sessionStore: SessionStore; }): Promise { - return appLogDurableResource.forceCleanupLive(params); + return appLogDurableResource.forceCleanupLive({ + binding: bindSessionAppLog(params.sessionStore, params.ref), + }); } export function recordSessionAppLogFailure(params: { diff --git a/src/daemon/handlers/record-runtime.ts b/src/daemon/handlers/record-runtime.ts index 1e63e684cb..ad8cfc5b16 100644 --- a/src/daemon/handlers/record-runtime.ts +++ b/src/daemon/handlers/record-runtime.ts @@ -102,7 +102,13 @@ async function handleRecordCommandUnsafe( resolvedSession.needsReadiness, ); } - return await stopRecording(params, session, plan.kind, resolvedSession.needsReadiness); + return await stopRecording( + params, + session, + resolvedSession.ref, + plan.kind, + resolvedSession.needsReadiness, + ); } function resolveRecordPlan(req: DaemonRequest, session: SessionState | undefined) { @@ -230,6 +236,7 @@ function recordingAppIdentity( async function stopRecording( params: RecordRuntimeHandlerParams, session: SessionState, + ref: SessionRef | undefined, kind: 'stop-live' | 'stop-recovery', needsReadiness: boolean, ): Promise { @@ -240,15 +247,13 @@ async function stopRecording( ? { completion: await finishLiveScreenRecording({ intent: 'capture', - session, - sessionName: params.sessionName, - sessionStore: params.sessionStore, + binding: bindSessionScreenRecording(params.sessionStore, ref!), }), recordsSessionAction: true, } : await finishRecovered(params, session, needsReadiness); } catch (error) { - deleteTerminalRecordOnlySession(params, session); + deleteTerminalRecordOnlySession(params, session, ref); throw error; } const completion = stopped.completion; @@ -263,16 +268,17 @@ async function stopRecording( showTouches: completion.showTouches, }); } - if (session.recordOnlySession) params.sessionStore.delete(params.sessionName); + if (session.recordOnlySession && ref) params.sessionStore.retire(ref); return response; } function deleteTerminalRecordOnlySession( params: Pick, session: SessionState, + ref: SessionRef | undefined, ): void { - if (!session.recordOnlySession) return; - if (screenRecordingManifestIsTerminal(params)) params.sessionStore.delete(params.sessionName); + if (!session.recordOnlySession || !ref) return; + if (screenRecordingManifestIsTerminal(params)) params.sessionStore.retire(ref); } async function finishRecovered( diff --git a/src/daemon/request-execution-scope.ts b/src/daemon/request-execution-scope.ts index 2fcf80bc7c..b19eaa3bcd 100644 --- a/src/daemon/request-execution-scope.ts +++ b/src/daemon/request-execution-scope.ts @@ -418,12 +418,13 @@ async function teardownExpiredSession(params: { platformCleanup: PlatformResourceCleanup; }): Promise { const { session, sessionName, sessionStore, inspectFacts, bindDevice, platformCleanup } = params; + const ref = sessionStore.lookup(sessionName); + if (!ref) return; let primaryError: unknown; try { await teardownSessionResources({ appLog: 'run', - session, - sessionName, + ref, sessionStore, platformCleanup, }); diff --git a/src/daemon/server/daemon-runtime-device-claims.test.ts b/src/daemon/server/daemon-runtime-device-claims.test.ts index 97586767cc..05bfb11e22 100644 --- a/src/daemon/server/daemon-runtime-device-claims.test.ts +++ b/src/daemon/server/daemon-runtime-device-claims.test.ts @@ -47,7 +47,7 @@ test('finalizes provider state but does not clear a claim after shutdown teardow const afterSuccessfulTeardown = vi.fn(async () => {}); await teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stderr: { write: () => {} }, beforeDelete, @@ -67,7 +67,7 @@ test('finalizes provider state but does not clear a claim after shutdown teardow const afterSuccessfulTeardown = vi.fn(async () => {}); const teardown = teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stderr: { write: () => {} }, beforeDelete, diff --git a/src/daemon/server/daemon-runtime-recording-teardown.test.ts b/src/daemon/server/daemon-runtime-recording-teardown.test.ts index d0d9897465..f4eae37115 100644 --- a/src/daemon/server/daemon-runtime-recording-teardown.test.ts +++ b/src/daemon/server/daemon-runtime-recording-teardown.test.ts @@ -35,7 +35,7 @@ test('daemon shutdown awaits durable recording finalization inside its extended const stderrChunks: string[] = []; const teardown = teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stderr: { write: (chunk) => stderrChunks.push(chunk) }, }); @@ -72,7 +72,7 @@ test('daemon shutdown resolves durable recording resources through the effective const stderrChunks: string[] = []; await teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(effectiveSessionName)!, sessionStore, stateDir: root, stderr: { write: (chunk) => stderrChunks.push(chunk) }, diff --git a/src/daemon/server/daemon-runtime-web-close-teardown.test.ts b/src/daemon/server/daemon-runtime-web-close-teardown.test.ts index dbd971d386..64e8d377de 100644 --- a/src/daemon/server/daemon-runtime-web-close-teardown.test.ts +++ b/src/daemon/server/daemon-runtime-web-close-teardown.test.ts @@ -49,7 +49,7 @@ test('daemon shutdown awaits a slow web close inside its extended budget', async const stderrChunks: string[] = []; const teardown = teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stateDir: root, stderr: { write: (chunk) => stderrChunks.push(chunk) }, @@ -83,7 +83,7 @@ test('daemon shutdown closes an open web session immediately, without waiting fo const stderrChunks: string[] = []; await teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stateDir: root, stderr: { write: (chunk) => stderrChunks.push(chunk) }, @@ -116,7 +116,7 @@ test('daemon shutdown reports a web close failure on stderr instead of losing it const stderrChunks: string[] = []; await teardownDaemonSessionForShutdown({ - session, + ref: sessionStore.lookup(session.name)!, sessionStore, stateDir: root, stderr: { write: (chunk) => stderrChunks.push(chunk) }, diff --git a/src/daemon/server/daemon-runtime.ts b/src/daemon/server/daemon-runtime.ts index fe73e03097..499cabeea1 100644 --- a/src/daemon/server/daemon-runtime.ts +++ b/src/daemon/server/daemon-runtime.ts @@ -35,7 +35,7 @@ import { finalizeDaemonSessionApplicationLifecycle } from '../application-lifecy import { runtimeHintValues } from '../session-runtime.ts'; import { closeDaemonServers } from './server-shutdown.ts'; import type { DaemonInvokeFn } from '../daemon-request.ts'; -import type { SessionState } from '../session-state.ts'; +import type { SessionRef, SessionState } from '../session-state.ts'; import { createDaemonIdleReap } from './daemon-idle-reap.ts'; import { createSessionIdleExpiry } from './daemon-session-idle-expiry.ts'; import { resolveSessionIdleExpiryMs } from '../session-idle-expiry.ts'; @@ -133,7 +133,7 @@ async function settleDaemonTeardownStep(params: { * silently swallowed. */ export async function teardownDaemonSessionForShutdown(params: { - session: SessionState; + ref: SessionRef; sessionStore: SessionStore; stateDir?: string; stderr: WritableOutput; @@ -142,7 +142,7 @@ export async function teardownDaemonSessionForShutdown(params: { afterSuccessfulTeardown?: (session: SessionState) => Promise; }): Promise { const { - session, + ref, sessionStore, stateDir, stderr, @@ -150,7 +150,7 @@ export async function teardownDaemonSessionForShutdown(params: { beforeDelete, afterSuccessfulTeardown, } = params; - const sessionName = sessionStore.resolveStoredSessionName(session); + const session = sessionStore.resolveCurrent(ref) ?? ref.session; const timeoutMs = resolveDaemonSessionTeardownTimeoutMs(session); // The ownership-fenced app-log side effect must settle while this process // still owns the daemon lock. It is intentionally outside the generic @@ -160,9 +160,9 @@ export async function teardownDaemonSessionForShutdown(params: { session, stderr, resource: 'app-log', - teardown: async () => await stopSessionAppLog({ session, sessionName, sessionStore }), + teardown: async () => await stopSessionAppLog({ ref, sessionStore }), }); - const sessionAfterAppLog = sessionStore.get(sessionName) ?? session; + const sessionAfterAppLog = sessionStore.resolveCurrent(ref) ?? session; const teardown = (async () => { const genericTeardownSucceeded = await settleDaemonTeardownStep({ session, @@ -171,8 +171,7 @@ export async function teardownDaemonSessionForShutdown(params: { teardown: async () => await teardownSessionResources({ appLog: 'already-settled', - session: sessionAfterAppLog, - sessionName, + ref, sessionStore, stateDir, platformCleanup: platformResourceCleanup, @@ -202,7 +201,7 @@ export async function teardownDaemonSessionForShutdown(params: { sessionStore.finalizeRepairTeardown(session); await beforeDelete?.(session); if (teardownSucceeded) await afterSuccessfulTeardown?.(session); - sessionStore.delete(sessionName); + sessionStore.retire(ref); } export type DaemonRuntimeOptions = { @@ -457,10 +456,11 @@ export async function startDaemonRuntime( const shutdownClaimLedger = createDaemonShutdownClaimLedger(); - const teardownDaemonSession = async (session: SessionState): Promise => { + const teardownDaemonSession = async (ref: SessionRef): Promise => { + const session = sessionStore.resolveCurrent(ref) ?? ref.session; try { await teardownDaemonSessionForShutdown({ - session, + ref, sessionStore, stderr, finalizeApplicationLifecycle: async (sessionToFinalize) => @@ -487,7 +487,7 @@ export async function startDaemonRuntime( }; const teardownDaemonSessions = async (): Promise => { - const sessionsToStop = sessionStore.toArray(); + const sessionsToStop = sessionStore.listRefs(); await Promise.all(sessionsToStop.map(teardownDaemonSession)); }; @@ -503,10 +503,11 @@ export async function startDaemonRuntime( session: SessionState, sessionName: string, ): Promise => { + const ref = sessionStore.lookup(sessionName); + if (!ref) return; await teardownSessionResources({ appLog: 'run', - session, - sessionName, + ref, sessionStore, stateDir: baseDir, platformCleanup: platformResourceCleanup, diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts index d2ffe590f8..3a90103ced 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-teardown-resources.test.ts @@ -109,14 +109,12 @@ test('daemon resource teardown finalizes recording before lifecycle runner dispo await teardownSessionResources({ appLog: 'already-settled', - session, - sessionName, + ref: sessionStore.lookup(sessionName)!, sessionStore, }); await teardownSessionResources({ appLog: 'already-settled', - session, - sessionName, + ref: sessionStore.lookup(sessionName)!, sessionStore, }); @@ -145,8 +143,7 @@ test('daemon session teardown surfaces a recording finalization failure', async await expect( teardownSessionResources({ appLog: 'already-settled', - session, - sessionName, + ref: sessionStore.lookup(sessionName)!, sessionStore, }), ).rejects.toThrow(/recording: .*failed to stop recording/); @@ -170,8 +167,7 @@ test('daemon session teardown retains recording evidence when finish and forced await expect( teardownSessionResources({ appLog: 'already-settled', - session, - sessionName, + ref: sessionStore.lookup(sessionName)!, sessionStore, }), ).rejects.toThrow(/recording: .*failed to stop recording/); @@ -206,7 +202,8 @@ test('daemon session teardown stops Android snapshot helper session', async () = } as SessionState; const sessionStore = makeSessionStore(); - await teardownSessionResources({ appLog: 'already-settled', session, sessionName, sessionStore }); + const ref = sessionStore.publish(sessionName, session); + await teardownSessionResources({ appLog: 'already-settled', ref, sessionStore }); expect(mockStopAndroidSnapshotHelperSessionForDevice).toHaveBeenCalledWith(session.device); }); @@ -237,8 +234,7 @@ test('daemon session teardown attempts every resource after an earlier cleanup r await expect( teardownSessionResources({ appLog: 'already-settled', - session, - sessionName, + ref: sessionStore.lookup(sessionName)!, sessionStore, }), ).rejects.toMatchObject({ @@ -286,7 +282,11 @@ test('daemon session teardown closes an open web session immediately, not on age sessionStore.set(sessionName, session); mockRunCmd.mockResolvedValue(agentBrowserJsonResult({ success: true, data: {} })); - await teardownSessionResources({ appLog: 'already-settled', session, sessionName, sessionStore }); + await teardownSessionResources({ + appLog: 'already-settled', + ref: sessionStore.lookup(sessionName)!, + sessionStore, + }); // A SIGTERM daemon shutdown (or an expired-session reap) tells agent-browser to close its // fleet right away, the same way an explicit `session close` does, instead of leaving the @@ -309,7 +309,11 @@ test('daemon session teardown surfaces a web close failure through the cleanup-f ); await expect( - teardownSessionResources({ appLog: 'already-settled', session, sessionName, sessionStore }), + teardownSessionResources({ + appLog: 'already-settled', + ref: sessionStore.lookup(sessionName)!, + sessionStore, + }), ).rejects.toMatchObject({ code: 'COMMAND_FAILED', details: expect.objectContaining({ @@ -329,11 +333,12 @@ test('daemon session teardown never dispatches a web close for a non-web session booted: true, }); + const sessionStore = makeSessionStore(); + const ref = sessionStore.publish(sessionName, session); await teardownSessionResources({ appLog: 'already-settled', - session, - sessionName, - sessionStore: makeSessionStore(), + ref, + sessionStore, }); expect(mockRunCmd).not.toHaveBeenCalled(); diff --git a/src/daemon/session-lifecycle/internal/session-close-lifecycle-teardown.ts b/src/daemon/session-lifecycle/internal/session-close-lifecycle-teardown.ts index 4dd4fe6c77..81eb0b82bd 100644 --- a/src/daemon/session-lifecycle/internal/session-close-lifecycle-teardown.ts +++ b/src/daemon/session-lifecycle/internal/session-close-lifecycle-teardown.ts @@ -134,21 +134,11 @@ async function stopBestEffortSessionResources( attemptCleanup: CleanupRunner, platformCleanup: PlatformResourceCleanup, ): Promise { - const { address: sessionName, session } = ref; - // Recording overlay finalization needs the Apple runner, so it runs first. - // `finishSessionScreenRecording` re-reads the stored session by address and - // returns when there is no recording; a second lookup here would only be a - // place to mis-address it. - await attemptCleanup('recording', () => - finishSessionScreenRecording({ session, sessionName, sessionStore }), - ); - await attemptCleanup('app_log', () => stopSessionAppLog({ session, sessionName, sessionStore })); - await attemptCleanup('audio_probe', () => - finishSessionAudioProbe({ session, sessionName, sessionStore }), - ); - await attemptCleanup('perf_capture', () => - stopSessionPerfCapture({ session, sessionName, sessionStore }), - ); + const session = sessionStore.resolveCurrent(ref) ?? ref.session; + await attemptCleanup('recording', () => finishSessionScreenRecording({ ref, sessionStore })); + await attemptCleanup('app_log', () => stopSessionAppLog({ ref, sessionStore })); + await attemptCleanup('audio_probe', () => finishSessionAudioProbe({ ref, sessionStore })); + await attemptCleanup('perf_capture', () => stopSessionPerfCapture({ ref, sessionStore })); await attemptCleanup('platform_snapshot_helper', () => stopSessionSnapshotHelper(session, platformCleanup), ); diff --git a/src/daemon/session-observability/internal/session-audio.ts b/src/daemon/session-observability/internal/session-audio.ts index 6d439e65b5..2d5292c1ea 100644 --- a/src/daemon/session-observability/internal/session-audio.ts +++ b/src/daemon/session-observability/internal/session-audio.ts @@ -20,7 +20,7 @@ import type { } from '../../request-runtime-binding.ts'; import type { SessionStore } from '../../session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; -import type { SessionRef, SessionState } from '../../session-state.ts'; +import type { SessionRef } from '../../session-state.ts'; import { bindSessionAudioProbe } from '../../audio-probe-session-binding.ts'; import { type DaemonFailureResponse, errorResponse } from '@agent-device/kernel/contracts'; @@ -71,9 +71,9 @@ async function handleAudioCommandUnsafe(params: AudioParams): Promise { - const probe = session.audioProbe; +async function audioProbeStatus(params: AudioParams, ref: SessionRef): Promise { + const binding = bindSessionAudioProbe(params.sessionStore, ref); + const probe = binding.read(); if (!probe) return { ok: true, data: inactiveAudioProbeResult() }; const data = await probe.handle.status(); if (data.state === 'stopped') { @@ -166,21 +162,18 @@ async function audioProbeStatus( // status, never for an export no later stop could produce from a dead sampler. await finishLiveAudioProbe({ intent: 'disposal', - session, - sessionName: params.sessionName, - sessionStore: params.sessionStore, + binding, }); } return { ok: true, data }; } -async function stopAudioProbe(params: AudioParams, session: SessionState): Promise { - if (!session.audioProbe) return { ok: true, data: inactiveAudioProbeResult() }; +async function stopAudioProbe(params: AudioParams, ref: SessionRef): Promise { + const binding = bindSessionAudioProbe(params.sessionStore, ref); + if (!binding.read()) return { ok: true, data: inactiveAudioProbeResult() }; const completion = await finishLiveAudioProbe({ intent: 'capture', - session, - sessionName: params.sessionName, - sessionStore: params.sessionStore, + binding, }); return { ok: true, data: completion }; } diff --git a/src/daemon/session-observability/internal/session-observability.ts b/src/daemon/session-observability/internal/session-observability.ts index 2e98bd25de..7d00c3cba8 100644 --- a/src/daemon/session-observability/internal/session-observability.ts +++ b/src/daemon/session-observability/internal/session-observability.ts @@ -323,10 +323,8 @@ async function handleLogsClearRestart( // an open record left here would refuse the start this path exists to serve. await finishSessionAppLog({ intent: 'disposal', - session, - sessionName, + ref: params.ref, sessionStore, - resourcePath: appLogResourceStore.resolvePath(sessionStore.resolveSessionDir(sessionName)), }); } const logPath = sessionStore.resolveAppLogPath(sessionName); @@ -356,10 +354,8 @@ async function handleLogsStop(params: LogsHandlerParams): Promise, ): Promise { @@ -263,14 +263,11 @@ async function stopPerfCapture( } const completion = await finishLivePerfCapture({ intent: 'capture', - session, - sessionName: params.sessionName, - sessionStore: params.sessionStore, + binding: bindSessionPerfCapture(params.sessionStore, params.ref), }); if (request.area === 'cpu') { const profile = readProfileHandoff(completion); - const refreshed = params.sessionStore.get(params.sessionName) ?? session; - params.sessionStore.set(params.sessionName, { ...refreshed, lastPerfProfile: profile }); + params.sessionStore.update(params.ref, { lastPerfProfile: profile }); } return { ok: true, data: completion }; } diff --git a/src/daemon/session-teardown.ts b/src/daemon/session-teardown.ts index 2f051b2057..65f87445d6 100644 --- a/src/daemon/session-teardown.ts +++ b/src/daemon/session-teardown.ts @@ -1,9 +1,11 @@ import { AppError } from '@agent-device/kernel/errors'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { cleanupRetainedMaterializedPathsForSession } from './materialized-path-registry.ts'; -import type { SessionState } from './session-state.ts'; +import type { SessionRef, SessionState } from './session-state.ts'; +import { bindSessionAudioProbe } from './audio-probe-session-binding.ts'; +import { bindSessionPerfCapture } from './perf-capture-session-binding.ts'; +import { bindSessionScreenRecording } from './screen-recording-session-binding.ts'; import type { SessionStore } from './session-store.ts'; -import { appLogResourceStore } from './app-log-resource-store.ts'; import { finishLiveAudioProbe } from '@agent-device/capture-kit/audio-probe-session-resource'; import { finishLivePerfCapture } from '@agent-device/capture-kit/perf-capture-session-resource'; import { finishLiveScreenRecording } from '@agent-device/capture-kit/screen-recording-session-resource'; @@ -11,29 +13,19 @@ import { openWebSessionNames } from './web-session-names.ts'; import type { PlatformResourceCleanup } from './platform-resource-cleanup.ts'; export async function stopSessionAppLog(params: { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; }): Promise { - const { session, sessionName, sessionStore } = params; - if (!session.appLog) return; const { forceCleanupSessionAppLog } = await import('./app-log-session-resource.ts'); - await forceCleanupSessionAppLog({ - session, - sessionName, - sessionStore, - resourcePath: appLogResourceStore.resolvePath(sessionStore.resolveSessionDir(sessionName)), - }); + await forceCleanupSessionAppLog(params); } export async function stopSessionPerfCapture(params: { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; }): Promise { - const currentSession = params.sessionStore.get(params.sessionName) ?? params.session; - if (!currentSession.perfCapture) return; - await finishLivePerfCapture({ ...params, session: currentSession, intent: 'disposal' }); + const binding = bindSessionPerfCapture(params.sessionStore, params.ref); + if (binding.read()) await finishLivePerfCapture({ binding, intent: 'disposal' }); } export async function stopSessionSnapshotHelper( @@ -51,12 +43,13 @@ export async function stopSessionSnapshotHelper( // siblings above, this has no second caller in the ordinary-close path (that path already // reaches the browser through `dispatchTargetedPlatformClose`), so it stays module-private. async function stopSessionWebBrowser(params: { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; platformCleanup: PlatformResourceCleanup; }): Promise { - const { session, sessionName, sessionStore, platformCleanup } = params; + const { ref, sessionStore, platformCleanup } = params; + const session = sessionStore.resolveCurrent(ref) ?? ref.session; + const sessionName = ref.address; await platformCleanup.closeManagedBrowser({ device: session.device, sessionName, @@ -120,8 +113,7 @@ export function reportSessionCleanupFailures(params: { } type SessionResourceTeardownRequest = { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; stateDir?: string; appLog: 'run' | 'already-settled'; @@ -131,7 +123,9 @@ type SessionResourceTeardownRequest = { export async function teardownSessionResources( request: SessionResourceTeardownRequest, ): Promise { - const { session, sessionName, sessionStore } = request; + const { ref, sessionStore } = request; + const session = sessionStore.resolveCurrent(ref) ?? ref.session; + const sessionName = ref.address; if (!request.platformCleanup) { throw new AppError( 'INTERNAL_ERROR', @@ -144,7 +138,7 @@ export async function teardownSessionResources( ? [ { step: 'app_log', - run: () => stopSessionAppLog({ session, sessionName, sessionStore }), + run: () => stopSessionAppLog({ ref, sessionStore }), }, ] : []; @@ -158,19 +152,18 @@ export async function teardownSessionResources( step: 'recording', run: () => finishSessionScreenRecording({ - session, - sessionName, + ref, sessionStore, }), }, ...appLogSteps, { step: 'audio_probe', - run: () => finishSessionAudioProbe({ session, sessionName, sessionStore }), + run: () => finishSessionAudioProbe({ ref, sessionStore }), }, { step: 'perf_capture', - run: () => stopSessionPerfCapture({ session, sessionName, sessionStore }), + run: () => stopSessionPerfCapture({ ref, sessionStore }), }, { step: 'platform_snapshot_helper', @@ -183,8 +176,7 @@ export async function teardownSessionResources( step: 'web_browser', run: () => stopSessionWebBrowser({ - session, - sessionName, + ref, sessionStore, platformCleanup, }), @@ -204,31 +196,17 @@ export async function teardownSessionResources( } export async function finishSessionScreenRecording(params: { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; }): Promise { - const currentSession = params.sessionStore.get(params.sessionName) ?? params.session; - if (!currentSession.screenRecording) return; - await finishLiveScreenRecording({ - intent: 'disposal', - session: currentSession, - sessionName: params.sessionName, - sessionStore: params.sessionStore, - }); + const binding = bindSessionScreenRecording(params.sessionStore, params.ref); + if (binding.read()) await finishLiveScreenRecording({ binding, intent: 'disposal' }); } export async function finishSessionAudioProbe(params: { - session: SessionState; - sessionName: string; + ref: SessionRef; sessionStore: SessionStore; }): Promise { - const currentSession = params.sessionStore.get(params.sessionName) ?? params.session; - if (!currentSession.audioProbe) return; - await finishLiveAudioProbe({ - intent: 'disposal', - session: currentSession, - sessionName: params.sessionName, - sessionStore: params.sessionStore, - }); + const binding = bindSessionAudioProbe(params.sessionStore, params.ref); + if (binding.read()) await finishLiveAudioProbe({ binding, intent: 'disposal' }); } From d58cc91932967f7854d7de43f73418e7aa096525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 06:50:44 +0200 Subject: [PATCH 2/3] chore(gates): retire whole-session capture projections --- .../capture-kit/src/durable-capture/index.ts | 2 -- .../layering/session-resource-ownership.test.ts | 16 +++++++++++++++- scripts/layering/session-resource-ownership.ts | 8 +------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/capture-kit/src/durable-capture/index.ts b/packages/capture-kit/src/durable-capture/index.ts index dc4349e202..1d8f84b174 100644 --- a/packages/capture-kit/src/durable-capture/index.ts +++ b/packages/capture-kit/src/durable-capture/index.ts @@ -12,10 +12,8 @@ export type { AdoptStartedDurableCaptureParams, DurableCaptureFinishIntent, DurableCaptureRecordDefinition, - DurableCaptureResourceDefinition, DurableCaptureSessionResource, DurableCaptureSessionBinding, - DurableCaptureSessionStore, } from './definition.ts'; export type { FinishRecoveredDurableCaptureParams } from './finish-recovered.ts'; export type { diff --git a/scripts/layering/session-resource-ownership.test.ts b/scripts/layering/session-resource-ownership.test.ts index 4b2e7fc2b8..4fd9fd7045 100644 --- a/scripts/layering/session-resource-ownership.test.ts +++ b/scripts/layering/session-resource-ownership.test.ts @@ -24,7 +24,19 @@ test('session resources are constructed only by their durable domain owners', () ], [ 'src/daemon/app-log-session-resource.ts', - `sessionStore.set(name, { ...session, appLog: log, appLogFailure: undefined });`, + `sessionStore.update(ref, { appLog: log, appLogFailure: undefined });`, + ], + [ + 'src/daemon/audio-probe-session-binding.ts', + `sessionStore.update(ref, { audioProbe: audio });`, + ], + [ + 'src/daemon/perf-capture-session-binding.ts', + `sessionStore.update(ref, { perfCapture: perf });`, + ], + [ + 'src/daemon/screen-recording-session-binding.ts', + `sessionStore.update(ref, { screenRecording: recording });`, ], [ 'packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts', @@ -41,6 +53,8 @@ test('session resources are constructed only by their durable domain owners', () 'src/daemon/handlers/planted.ts: session audioProbe record constructed outside its owner', 'src/daemon/handlers/planted.ts: session perfCapture record constructed outside its owner', 'src/daemon/handlers/planted.ts: session screenRecording record constructed outside its owner', + 'packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts: session audioProbe record constructed outside its owner', + 'packages/capture-kit/src/capture-admission/perf-capture-session-resource.ts: session perfCapture record constructed outside its owner', ], ); }); diff --git a/scripts/layering/session-resource-ownership.ts b/scripts/layering/session-resource-ownership.ts index 579dd3d637..24408f22b4 100644 --- a/scripts/layering/session-resource-ownership.ts +++ b/scripts/layering/session-resource-ownership.ts @@ -27,18 +27,12 @@ const SCANNED_ROOTS = ['src/daemon/', 'packages/capture-kit/src/capture-admissio const RESOURCE_OWNERS: Readonly>> = { appLog: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']), appLogFailure: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']), - audioProbe: new Set([ - 'packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts', - 'src/daemon/audio-probe-session-binding.ts', - 'src/daemon/session-state.ts', - ]), + audioProbe: new Set(['src/daemon/audio-probe-session-binding.ts', 'src/daemon/session-state.ts']), screenRecording: new Set([ - 'packages/capture-kit/src/capture-admission/screen-recording-session-resource.ts', 'src/daemon/screen-recording-session-binding.ts', 'src/daemon/session-state.ts', ]), perfCapture: new Set([ - 'packages/capture-kit/src/capture-admission/perf-capture-session-resource.ts', 'src/daemon/perf-capture-session-binding.ts', 'src/daemon/session-state.ts', ]), From e0a818cd55e24d5982721683add83ecd494178fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 18:52:05 +0200 Subject: [PATCH 3/3] refactor: keep session capture bindings in one module Session teardown imported three per-capture binding modules plus their shared binding, which grew its eager closure past the merge-base. The audio, perf and screen-recording bindings now live beside bindSessionCapture. --- .../session-resource-ownership.test.ts | 14 +++------- .../layering/session-resource-ownership.ts | 11 +++----- .../perf-capture-session-resource.test.ts | 2 +- .../__tests__/session-capture-binding.test.ts | 2 +- src/daemon/audio-probe-session-binding.ts | 12 --------- src/daemon/handlers/record-runtime.ts | 6 ++--- src/daemon/perf-capture-session-binding.ts | 12 --------- .../screen-recording-session-binding.ts | 11 +------- src/daemon/session-capture-binding.ts | 27 +++++++++++++++++++ .../internal/session-audio.ts | 2 +- .../internal/session-perf-runtime.ts | 2 +- src/daemon/session-teardown.ts | 8 +++--- 12 files changed, 47 insertions(+), 62 deletions(-) delete mode 100644 src/daemon/audio-probe-session-binding.ts delete mode 100644 src/daemon/perf-capture-session-binding.ts diff --git a/scripts/layering/session-resource-ownership.test.ts b/scripts/layering/session-resource-ownership.test.ts index 4fd9fd7045..8a1453e6cb 100644 --- a/scripts/layering/session-resource-ownership.test.ts +++ b/scripts/layering/session-resource-ownership.test.ts @@ -27,16 +27,10 @@ test('session resources are constructed only by their durable domain owners', () `sessionStore.update(ref, { appLog: log, appLogFailure: undefined });`, ], [ - 'src/daemon/audio-probe-session-binding.ts', - `sessionStore.update(ref, { audioProbe: audio });`, - ], - [ - 'src/daemon/perf-capture-session-binding.ts', - `sessionStore.update(ref, { perfCapture: perf });`, - ], - [ - 'src/daemon/screen-recording-session-binding.ts', - `sessionStore.update(ref, { screenRecording: recording });`, + 'src/daemon/session-capture-binding.ts', + `sessionStore.update(ref, { audioProbe: audio }); + sessionStore.update(ref, { perfCapture: perf }); + sessionStore.update(ref, { screenRecording: recording });`, ], [ 'packages/capture-kit/src/capture-admission/audio-probe-session-resource.ts', diff --git a/scripts/layering/session-resource-ownership.ts b/scripts/layering/session-resource-ownership.ts index 24408f22b4..323d8af3be 100644 --- a/scripts/layering/session-resource-ownership.ts +++ b/scripts/layering/session-resource-ownership.ts @@ -1,4 +1,4 @@ -// Catches: a session resource field (appLog, appLogFailure, audioProbe, perfCapture) written +// Catches: a session resource field (appLog, appLogFailure, audioProbe, perfCapture, screenRecording) written // from outside its declared owner module — R7's session-state-ownership shape applied to the // narrower set of per-resource fields these session-scoped runtimes carry, where the same // aliasing hazard (get()/set() hand back and re-put the live reference) applies. @@ -27,15 +27,12 @@ const SCANNED_ROOTS = ['src/daemon/', 'packages/capture-kit/src/capture-admissio const RESOURCE_OWNERS: Readonly>> = { appLog: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']), appLogFailure: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']), - audioProbe: new Set(['src/daemon/audio-probe-session-binding.ts', 'src/daemon/session-state.ts']), + audioProbe: new Set(['src/daemon/session-capture-binding.ts', 'src/daemon/session-state.ts']), screenRecording: new Set([ - 'src/daemon/screen-recording-session-binding.ts', - 'src/daemon/session-state.ts', - ]), - perfCapture: new Set([ - 'src/daemon/perf-capture-session-binding.ts', + 'src/daemon/session-capture-binding.ts', 'src/daemon/session-state.ts', ]), + perfCapture: new Set(['src/daemon/session-capture-binding.ts', 'src/daemon/session-state.ts']), }; /** Durable session-resource records have one whole-record construction owner per domain. */ diff --git a/src/daemon/__tests__/perf-capture-session-resource.test.ts b/src/daemon/__tests__/perf-capture-session-resource.test.ts index ecd14eb954..6d47f62998 100644 --- a/src/daemon/__tests__/perf-capture-session-resource.test.ts +++ b/src/daemon/__tests__/perf-capture-session-resource.test.ts @@ -1,4 +1,4 @@ -import { bindSessionPerfCapture } from '../perf-capture-session-binding.ts'; +import { bindSessionPerfCapture } from '../session-capture-binding.ts'; import { beforeEach, expect, test, vi } from 'vitest'; import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime'; import { AppError } from '@agent-device/kernel/errors'; diff --git a/src/daemon/__tests__/session-capture-binding.test.ts b/src/daemon/__tests__/session-capture-binding.test.ts index 41f5322d5b..423c192eed 100644 --- a/src/daemon/__tests__/session-capture-binding.test.ts +++ b/src/daemon/__tests__/session-capture-binding.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'vitest'; import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; import { makeRecordingSession } from './session-teardown.fixtures.ts'; -import { bindSessionScreenRecording } from '../screen-recording-session-binding.ts'; +import { bindSessionScreenRecording } from '../session-capture-binding.ts'; test('clearing a capture refreshes a rebuilt record without losing its other changes', () => { const store = makeSessionStore(); diff --git a/src/daemon/audio-probe-session-binding.ts b/src/daemon/audio-probe-session-binding.ts deleted file mode 100644 index b4eecb43c7..0000000000 --- a/src/daemon/audio-probe-session-binding.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { bindSessionCapture } from './session-capture-binding.ts'; -import type { SessionRef } from './session-state.ts'; -import type { SessionStore } from './session-store.ts'; - -export function bindSessionAudioProbe(sessionStore: SessionStore, ref: SessionRef) { - return bindSessionCapture(sessionStore, ref, { - read: (session) => session.audioProbe, - write: (audioProbe) => { - sessionStore.update(ref, { audioProbe }); - }, - }); -} diff --git a/src/daemon/handlers/record-runtime.ts b/src/daemon/handlers/record-runtime.ts index ad8cfc5b16..a44632387f 100644 --- a/src/daemon/handlers/record-runtime.ts +++ b/src/daemon/handlers/record-runtime.ts @@ -31,10 +31,8 @@ import type { SessionStore } from '../session-store.ts'; import type { BindDeviceRuntime, BindExactDeviceRuntime } from '../request-runtime-binding.ts'; import type { DaemonRequest, DaemonResponse } from '../daemon-request.ts'; import type { SessionRef, SessionState } from '../session-state.ts'; -import { - bindRecordOnlyScreenRecording, - bindSessionScreenRecording, -} from '../screen-recording-session-binding.ts'; +import { bindRecordOnlyScreenRecording } from '../screen-recording-session-binding.ts'; +import { bindSessionScreenRecording } from '../session-capture-binding.ts'; import { recordSessionAction } from '../session-action-recorder.ts'; import { missingAppSessionResponse, diff --git a/src/daemon/perf-capture-session-binding.ts b/src/daemon/perf-capture-session-binding.ts deleted file mode 100644 index 53463cd8ab..0000000000 --- a/src/daemon/perf-capture-session-binding.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { bindSessionCapture } from './session-capture-binding.ts'; -import type { SessionRef } from './session-state.ts'; -import type { SessionStore } from './session-store.ts'; - -export function bindSessionPerfCapture(sessionStore: SessionStore, ref: SessionRef) { - return bindSessionCapture(sessionStore, ref, { - read: (session) => session.perfCapture, - write: (perfCapture) => { - sessionStore.update(ref, { perfCapture }); - }, - }); -} diff --git a/src/daemon/screen-recording-session-binding.ts b/src/daemon/screen-recording-session-binding.ts index d032f819ba..b5d56813d0 100644 --- a/src/daemon/screen-recording-session-binding.ts +++ b/src/daemon/screen-recording-session-binding.ts @@ -1,16 +1,7 @@ -import { bindSessionCapture } from './session-capture-binding.ts'; +import { bindSessionScreenRecording } from './session-capture-binding.ts'; import type { SessionRef } from './session-state.ts'; import type { SessionStore } from './session-store.ts'; -export function bindSessionScreenRecording(sessionStore: SessionStore, ref: SessionRef) { - return bindSessionCapture(sessionStore, ref, { - read: (session) => session.screenRecording, - write: (screenRecording) => { - sessionStore.update(ref, { screenRecording }); - }, - }); -} - export function bindRecordOnlyScreenRecording( sessionStore: SessionStore, address: string, diff --git a/src/daemon/session-capture-binding.ts b/src/daemon/session-capture-binding.ts index c3f809cf37..ec7d7683fa 100644 --- a/src/daemon/session-capture-binding.ts +++ b/src/daemon/session-capture-binding.ts @@ -51,3 +51,30 @@ export function bindSessionCapture( }, }); } + +export function bindSessionAudioProbe(sessionStore: SessionStore, ref: SessionRef) { + return bindSessionCapture(sessionStore, ref, { + read: (session) => session.audioProbe, + write: (audioProbe) => { + sessionStore.update(ref, { audioProbe }); + }, + }); +} + +export function bindSessionPerfCapture(sessionStore: SessionStore, ref: SessionRef) { + return bindSessionCapture(sessionStore, ref, { + read: (session) => session.perfCapture, + write: (perfCapture) => { + sessionStore.update(ref, { perfCapture }); + }, + }); +} + +export function bindSessionScreenRecording(sessionStore: SessionStore, ref: SessionRef) { + return bindSessionCapture(sessionStore, ref, { + read: (session) => session.screenRecording, + write: (screenRecording) => { + sessionStore.update(ref, { screenRecording }); + }, + }); +} diff --git a/src/daemon/session-observability/internal/session-audio.ts b/src/daemon/session-observability/internal/session-audio.ts index 2d5292c1ea..104709368d 100644 --- a/src/daemon/session-observability/internal/session-audio.ts +++ b/src/daemon/session-observability/internal/session-audio.ts @@ -21,7 +21,7 @@ import type { import type { SessionStore } from '../../session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import type { SessionRef } from '../../session-state.ts'; -import { bindSessionAudioProbe } from '../../audio-probe-session-binding.ts'; +import { bindSessionAudioProbe } from '../../session-capture-binding.ts'; import { type DaemonFailureResponse, errorResponse } from '@agent-device/kernel/contracts'; type AudioParams = { diff --git a/src/daemon/session-observability/internal/session-perf-runtime.ts b/src/daemon/session-observability/internal/session-perf-runtime.ts index 4c8b4142e0..3e3fa1d11c 100644 --- a/src/daemon/session-observability/internal/session-perf-runtime.ts +++ b/src/daemon/session-observability/internal/session-perf-runtime.ts @@ -32,7 +32,7 @@ import type { import type { SessionStore } from '../../session-store.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; import type { SessionRef, SessionState } from '../../session-state.ts'; -import { bindSessionPerfCapture } from '../../perf-capture-session-binding.ts'; +import { bindSessionPerfCapture } from '../../session-capture-binding.ts'; import { recordSessionAction } from '../../session-action-recorder.ts'; import { admitRuntimePlan, diff --git a/src/daemon/session-teardown.ts b/src/daemon/session-teardown.ts index 65f87445d6..6b578e7328 100644 --- a/src/daemon/session-teardown.ts +++ b/src/daemon/session-teardown.ts @@ -2,9 +2,11 @@ import { AppError } from '@agent-device/kernel/errors'; import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import { cleanupRetainedMaterializedPathsForSession } from './materialized-path-registry.ts'; import type { SessionRef, SessionState } from './session-state.ts'; -import { bindSessionAudioProbe } from './audio-probe-session-binding.ts'; -import { bindSessionPerfCapture } from './perf-capture-session-binding.ts'; -import { bindSessionScreenRecording } from './screen-recording-session-binding.ts'; +import { + bindSessionAudioProbe, + bindSessionPerfCapture, + bindSessionScreenRecording, +} from './session-capture-binding.ts'; import type { SessionStore } from './session-store.ts'; import { finishLiveAudioProbe } from '@agent-device/capture-kit/audio-probe-session-resource'; import { finishLivePerfCapture } from '@agent-device/capture-kit/perf-capture-session-resource';