refactor(agent-core-v2): fold session lifecycle hooks into sessionLifecycle events (#2896)

This commit is contained in:
Haozhe 2026-08-13 21:03:51 +08:00 committed by GitHub
parent 5857ba23b0
commit 1414d46028
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 91 additions and 133 deletions

View file

@ -416,7 +416,6 @@ export * from '#/workspace/workspaceContext/workspaceContext';
export * from '#/workspace/sessionLifecycle/sessionLifecycle';
export * from '#/workspace/sessionLifecycle/sessionLifecycleService';
export * from '#/workspace/sessionLifecycle/internal/addressing';
export * from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
export * from '#/session/externalHooks/externalHooks';
export * from '#/session/externalHooks/externalHooksService';
import '#/app/sessionExport/errors';

View file

@ -28,24 +28,22 @@ import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { IntervalTimer } from '#/_base/utils/timer';
import { IExternalHooksRunnerService } from '#/app/externalHooksRunner/externalHooksRunner';
import type { Hooks } from '#/hooks';
import { IModelService } from '#/kosong/model/model';
import {
ISessionAgentProfileCatalog,
} from '#/session/sessionAgentProfileCatalog/sessionAgentProfileCatalog';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import {
ISessionLifecycleHooks,
type SessionCloseReason,
type SessionCreateSource,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
import {
type AgentTaskStartHookContext,
type AgentTaskStopHookContext,
ISessionSubagentService,
} from '#/session/subagent/subagent';
import {
ISessionLifecycleService,
type SessionCloseReason,
type SessionCreateSource,
} from '#/workspace/sessionLifecycle/sessionLifecycle';
import { ISessionExternalHooksService } from './externalHooks';
@ -64,7 +62,7 @@ export class SessionExternalHooksService
constructor(
@ISessionContext private readonly context: ISessionContext,
@ISessionLifecycleHooks lifecycleHooks: Hooks<SessionLifecycleHookSlots>,
@ISessionLifecycleService lifecycle: ISessionLifecycleService,
@ISessionSubagentService subagents: ISessionSubagentService,
@ISessionMetadata private readonly metadata: ISessionMetadata,
@ISessionAgentProfileCatalog private readonly profiles: ISessionAgentProfileCatalog,
@ -90,17 +88,17 @@ export class SessionExternalHooksService
}),
);
this._register(
lifecycleHooks.onDidCreateSession.register('externalHooks', async (event, next) => {
lifecycle.onDidCreateSession((event) => {
if (event.sessionId !== this.context.sessionId) return;
if (event.source !== 'fork') {
await this.triggerSessionStart(event.source);
event.waitUntil(this.triggerSessionStart(event.source));
}
await next();
}),
);
this._register(
lifecycleHooks.onWillCloseSession.register('externalHooks', async (event, next) => {
await this.triggerSessionEnd(event.reason);
await next();
lifecycle.onWillCloseSession((event) => {
if (event.sessionId !== this.context.sessionId) return;
event.waitUntil(this.triggerSessionEnd(event.reason));
}),
);
this._register(

View file

@ -1,36 +0,0 @@
/**
* `sessionLifecycleHooks` domain per-session lifecycle hook slots.
*
* Defines the `ISessionLifecycleHooks` seed: one ordered hook-slots instance
* per session, with slots around the session's create (`onDidCreateSession`)
* and close (`onWillCloseSession`). Also owns the shared
* `SessionCreateSource` / `SessionCloseReason` vocabulary.
*/
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { ScopeSeed } from '#/_base/di/scope';
import type { Hooks } from '#/hooks';
export type SessionCreateSource = 'startup' | 'resume' | 'fork';
export type SessionCloseReason = 'exit' | 'archive';
export interface SessionStartHookEvent {
readonly source: SessionCreateSource;
}
export interface SessionEndHookEvent {
readonly reason: SessionCloseReason;
}
export type SessionLifecycleHookSlots = {
readonly onDidCreateSession: SessionStartHookEvent;
readonly onWillCloseSession: SessionEndHookEvent;
};
export const ISessionLifecycleHooks: ServiceIdentifier<Hooks<SessionLifecycleHookSlots>> =
createDecorator<Hooks<SessionLifecycleHookSlots>>('sessionLifecycleHooks');
export function sessionLifecycleHooksSeed(hooks: Hooks<SessionLifecycleHookSlots>): ScopeSeed {
return [[ISessionLifecycleHooks as ServiceIdentifier<unknown>, hooks]];
}

View file

@ -24,15 +24,13 @@
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { ISessionScopeHandle } from '#/_base/di/scope';
import type { Event } from '#/_base/event';
import { type Event, type IWaitUntil } from '#/_base/event';
import type { BindAgentInput } from '#/agent/profile/profile';
import type { McpServerConfig } from '#/mcpCore/config-schema';
import type {
SessionCloseReason,
SessionCreateSource,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
export type { SessionCloseReason, SessionCreateSource };
export type SessionCreateSource = 'startup' | 'resume' | 'fork';
export type SessionCloseReason = 'exit' | 'archive';
export interface CreateSessionOptions {
readonly sessionId?: string;
@ -124,7 +122,8 @@ export interface ISessionLifecycleService {
readonly _serviceBrand: undefined;
readonly onWillCreateSession: Event<SessionWillCreateEvent>;
readonly onDidCreateSession: Event<SessionCreatedEvent>;
readonly onDidCreateSession: Event<SessionCreatedEvent & IWaitUntil>;
readonly onWillCloseSession: Event<SessionWillCloseEvent & IWaitUntil>;
readonly onDidCloseSession: Event<SessionClosedEvent>;
readonly onDidArchiveSession: Event<SessionArchivedEvent>;
readonly onDidForkSession: Event<SessionForkedEvent>;

View file

@ -101,7 +101,7 @@ import {
registerScopedService,
} from '#/_base/di/scope';
import { unwrapErrorCause } from '#/_base/errors/errors';
import { Emitter, type Event } from '#/_base/event';
import { AsyncEmitter, Emitter, type Event, type IWaitUntil } from '#/_base/event';
import { DEFAULT_PLAN_MODE_SECTION } from '#/features/plan/configSection';
import { IAgentPlanService } from '#/features/plan/plan';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
@ -118,7 +118,6 @@ import {
} from '#/app/sessionIndex/sessionIndex';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { ErrorCodes, Error2, isError2 } from '#/errors';
import { createHooks } from '#/hooks';
import { IHostEnvironment } from '#/os/interface/hostEnvironment';
import { IHostFileSystem, type HostDirEntry } from '#/os/interface/hostFileSystem';
import { IAppendLogStore } from '#/persistence/interface/appendLogStore';
@ -130,11 +129,6 @@ import { ISessionContext, sessionContextSeed } from '#/session/sessionContext/se
import { sessionEphemeralMcpServersSeed } from '#/session/mcp/ephemeralMcpServers';
import { sessionAgentProfileCatalogSeed } from '#/session/sessionAgentProfileCatalog/agentProfileCatalogSeed';
import { installSessionSeedAdapters } from '#/session/sessionSeed/sessionSeedAdapters';
import {
ISessionLifecycleHooks,
sessionLifecycleHooksSeed,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { ISessionMetadata, type SessionMeta } from '#/session/sessionMetadata/sessionMetadata';
import { drainSessionMetadataWrites, toEpochMs } from '#/session/sessionMetadata/sessionMetadataService';
import { ISessionProcessRunner } from '#/session/process/processRunner';
@ -183,6 +177,8 @@ type MaterializeSessionOptions = Omit<CreateSessionOptions, 'sessionId'> & {
readonly sessionId: string;
};
const NO_ABORT = new AbortController().signal;
// NOTE: stays Disposable — its own 'get' and 'config' collide with the Fiber
export class SessionLifecycleService extends Disposable implements ISessionLifecycleService {
declare readonly _serviceBrand: undefined;
@ -192,8 +188,16 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
);
readonly onWillCreateSession: Event<SessionWillCreateEvent> =
this._onWillCreateSession.event;
private readonly _onDidCreateSession = this._register(new Emitter<SessionCreatedEvent>());
readonly onDidCreateSession: Event<SessionCreatedEvent> = this._onDidCreateSession.event;
private readonly _onDidCreateSession = this._register(
new AsyncEmitter<SessionCreatedEvent & IWaitUntil>(),
);
readonly onDidCreateSession: Event<SessionCreatedEvent & IWaitUntil> =
this._onDidCreateSession.event;
private readonly _onWillCloseSession = this._register(
new AsyncEmitter<SessionWillCloseEvent & IWaitUntil>(),
);
readonly onWillCloseSession: Event<SessionWillCloseEvent & IWaitUntil> =
this._onWillCloseSession.event;
private readonly _onDidCloseSession = this._register(new Emitter<SessionClosedEvent>());
readonly onDidCloseSession: Event<SessionClosedEvent> = this._onDidCloseSession.event;
private readonly _onDidArchiveSession = this._register(new Emitter<SessionArchivedEvent>());
@ -295,10 +299,6 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
scope: (subKey?: string): string =>
subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`,
};
const hooks = createHooks<SessionLifecycleHookSlots, keyof SessionLifecycleHookSlots>([
'onDidCreateSession',
'onWillCloseSession',
]);
await this.hostEnv.ready;
const handle = createScopedChildHandle(
this.instantiation,
@ -307,7 +307,6 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
{
seeds: [
...sessionContextSeed(ctx),
...sessionLifecycleHooksSeed(hooks),
[ITelemetryService, this.telemetry.withContext({ sessionId: opts.sessionId })],
...sessionAgentProfileCatalogSeed({
_serviceBrand: undefined,
@ -364,10 +363,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
}
private async announceCreated(event: SessionCreatedEvent): Promise<void> {
await event.handle.accessor
.get(ISessionLifecycleHooks)
.onDidCreateSession.run({ source: event.source });
this._onDidCreateSession.fire(event);
await this._onDidCreateSession.fireAsync(event, NO_ABORT);
event.handle.accessor
.get(ITelemetryService)
.track2('session_started', { resumed: event.source === 'resume' });
@ -491,9 +487,7 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
}
private async announceWillClose(event: SessionWillCloseEvent): Promise<void> {
await event.handle.accessor
.get(ISessionLifecycleHooks)
.onWillCloseSession.run({ reason: event.reason });
await this._onWillCloseSession.fireAsync(event, NO_ABORT);
}
private async drainAgents(handle: ISessionScopeHandle): Promise<void> {

View file

@ -11,7 +11,7 @@ import {
createServices,
type TestInstantiationService,
} from '#/_base/di/test';
import { Emitter, Event } from '#/_base/event';
import { AsyncEmitter, Emitter, Event, type IWaitUntil } from '#/_base/event';
import { emptyUsage } from '#/kosong/contract/usage';
import { buildContextCompactionShape } from '#/agent/contextMemory/compactionHandoff';
import {
@ -47,10 +47,13 @@ import { IPluginService } from '#/app/plugin/plugin';
import { IHostProcessService } from '#/os/interface/hostProcess';
import { HostProcessService } from '#/os/backends/node-local/hostProcessService';
import {
ISessionLifecycleHooks,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { createHooks, type Hooks } from '#/hooks';
ISessionLifecycleService,
type SessionCloseReason,
type SessionCreatedEvent,
type SessionCreateSource,
type SessionWillCloseEvent,
} from '#/workspace/sessionLifecycle/sessionLifecycle';
import { createHooks } from '#/hooks';
import { ISessionContext } from '#/session/sessionContext/sessionContext';
import {
type AgentTaskHooks,
@ -229,11 +232,21 @@ function stubSessionContext(): ISessionContext {
};
}
function stubSessionLifecycleHooks(): Hooks<SessionLifecycleHookSlots> {
return createHooks<SessionLifecycleHookSlots, keyof SessionLifecycleHookSlots>([
'onDidCreateSession',
'onWillCloseSession',
]);
function stubSessionLifecycle() {
const didCreate = new AsyncEmitter<SessionCreatedEvent & IWaitUntil>();
const willClose = new AsyncEmitter<SessionWillCloseEvent & IWaitUntil>();
const noAbort = new AbortController().signal;
const handle = {} as ISessionScopeHandle;
return {
service: {
onDidCreateSession: didCreate.event,
onWillCloseSession: willClose.event,
},
fireDidCreate: (source: SessionCreateSource): Promise<void> =>
didCreate.fireAsync({ sessionId: 'session-1', handle, source }, noAbort),
fireWillClose: (reason: SessionCloseReason): Promise<void> =>
willClose.fireAsync({ sessionId: 'session-1', handle, reason }, noAbort),
};
}
describe('IExternalHooksRunnerService integration', () => {
@ -539,7 +552,7 @@ describe('IExternalHooksRunnerService integration', () => {
? 'sessions/workspace-1/session-1'
: `sessions/workspace-1/session-1/${subKey}`,
});
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
@ -856,7 +869,7 @@ describe('IExternalHooksRunnerService integration', () => {
const disposables = new DisposableStore();
let ix: TestInstantiationService | undefined;
try {
const lifecycleHooks = stubSessionLifecycleHooks();
const lifecycle = stubSessionLifecycle();
const path = hookLogPath();
const command = appendHookLogCommand(path);
const cwd = mkdtempSync(join(tmpdir(), 'session-external-hooks-cwd-'));
@ -877,7 +890,7 @@ describe('IExternalHooksRunnerService integration', () => {
? 'sessions/workspace-1/session-1'
: `sessions/workspace-1/session-1/${subKey}`,
});
reg.defineInstance(ISessionLifecycleHooks, lifecycleHooks);
reg.definePartialInstance(ISessionLifecycleService, lifecycle.service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
@ -907,11 +920,11 @@ describe('IExternalHooksRunnerService integration', () => {
ix.set(ISessionExternalHooksService, new SyncDescriptor(SessionExternalHooksService));
ix.get(ISessionExternalHooksService);
await lifecycleHooks.onDidCreateSession.run({ source: 'startup' });
await lifecycleHooks.onDidCreateSession.run({ source: 'resume' });
await lifecycleHooks.onDidCreateSession.run({ source: 'fork' });
await lifecycleHooks.onWillCloseSession.run({ reason: 'exit' });
await lifecycleHooks.onWillCloseSession.run({ reason: 'archive' });
await lifecycle.fireDidCreate('startup');
await lifecycle.fireDidCreate('resume');
await lifecycle.fireDidCreate('fork');
await lifecycle.fireWillClose('exit');
await lifecycle.fireWillClose('archive');
expect(readHookLog(path)).toEqual([
{
@ -1053,7 +1066,7 @@ describe('IExternalHooksRunnerService integration', () => {
const disposables = new DisposableStore();
let ix: TestInstantiationService | undefined;
try {
const lifecycleHooks = stubSessionLifecycleHooks();
const lifecycle = stubSessionLifecycle();
const path = hookLogPath();
const command = stdinScript([
'const fs = require("node:fs");',
@ -1074,7 +1087,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, lifecycleHooks);
reg.definePartialInstance(ISessionLifecycleService, lifecycle.service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata('My Session'));
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog('coder'));
reg.defineInstance(IModelService, stubModelService('kimi-k2'));
@ -1102,7 +1115,7 @@ describe('IExternalHooksRunnerService integration', () => {
ix.get(ISessionExternalHooksService);
await flushMicrotasks();
await lifecycleHooks.onDidCreateSession.run({ source: 'startup' });
await lifecycle.fireDidCreate('startup');
expect(readHookLog(path)).toEqual([
{
@ -1261,7 +1274,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
@ -1306,7 +1319,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());
@ -1353,7 +1366,7 @@ describe('IExternalHooksRunnerService integration', () => {
additionalServices: (reg) => {
registerStateServices(reg);
reg.defineInstance(ISessionContext, stubSessionContext());
reg.defineInstance(ISessionLifecycleHooks, stubSessionLifecycleHooks());
reg.definePartialInstance(ISessionLifecycleService, stubSessionLifecycle().service);
reg.defineInstance(ISessionMetadata, stubSessionMetadata());
reg.defineInstance(ISessionAgentProfileCatalog, stubProfileCatalog());
reg.defineInstance(IModelService, stubModelService());

View file

@ -95,6 +95,7 @@ describe('RestGateway', () => {
_serviceBrand: undefined,
onWillCreateSession: () => ({ dispose: () => {} }),
onDidCreateSession: () => ({ dispose: () => {} }),
onWillCloseSession: () => ({ dispose: () => {} }),
onDidCloseSession: () => ({ dispose: () => {} }),
onDidArchiveSession: () => ({ dispose: () => {} }),
onDidForkSession: () => ({ dispose: () => {} }),

View file

@ -918,6 +918,7 @@ function registerSessionExportServices(
_serviceBrand: undefined,
onWillCreateSession: noopEvent,
onDidCreateSession: noopEvent,
onWillCloseSession: noopEvent,
onDidCloseSession: noopEvent,
onDidArchiveSession: noopEvent,
onDidForkSession: noopEvent,

View file

@ -9,7 +9,7 @@ import { toDisposable } from '#/_base/di/lifecycle';
import type { IInstantiationService } from '#/_base/di/instantiation';
import type { IAgentScopeHandle } from '#/_base/di/scope';
import { IFeatureManager } from '#/app/feature/featureManager';
import { Emitter, Event } from '#/_base/event';
import { Emitter, Event, type IWaitUntil } from '#/_base/event';
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
import type { Promisable, PromisifyMethods } from '#/_base/utils/types';
import type { AgentTaskInfo } from '#/agent/task/task';
@ -172,11 +172,11 @@ import {
type ScopeSeed,
type ServiceIdentifier,
} from '#/index';
import { createHooks } from '#/hooks';
import {
ISessionLifecycleHooks,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
ISessionLifecycleService,
type SessionCreatedEvent,
type SessionWillCloseEvent,
} from '#/workspace/sessionLifecycle/sessionLifecycle';
import { IEventBus } from '#/app/event/eventBus';
import { IWireService } from '#/wire/wire';
import { WireService } from '#/wire/wireService';
@ -1209,13 +1209,10 @@ export class AgentTestContext {
scope: (subKey?: string): string =>
subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`,
});
reg.defineInstance(
ISessionLifecycleHooks,
createHooks<SessionLifecycleHookSlots, keyof SessionLifecycleHookSlots>([
'onDidCreateSession',
'onWillCloseSession',
]),
);
reg.definePartialInstance(ISessionLifecycleService, {
onDidCreateSession: Event.None as Event<SessionCreatedEvent & IWaitUntil>,
onWillCloseSession: Event.None as Event<SessionWillCloseEvent & IWaitUntil>,
});
reg.defineInstance(ISessionInteractionService, this.createInteractionService());
reg.defineInstance(ISessionApprovalService, this.createApprovalService());
reg.defineInstance(ISessionQuestionService, this.createQuestionService());

View file

@ -15,7 +15,6 @@ import {
import { type ScopedTestHost, createScopedTestHost, stubPair } from '#/_base/di/test';
import { Event } from '#/_base/event';
import { ILogService } from '#/_base/log/log';
import type { Hooks } from '#/hooks';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IConfigService } from '#/app/config/config';
import { IFlagService } from '#/app/flag/flag';
@ -59,10 +58,6 @@ import { IWorkspaceToolPolicy } from '#/workspace/workspaceToolPolicy/workspaceT
import { WorkspaceToolPolicyService } from '#/workspace/workspaceToolPolicy/workspaceToolPolicyService';
import { IAgentActivityView } from '#/agent/activityView/activityView';
import { ISessionExternalHooksService } from '#/session/externalHooks/externalHooks';
import {
ISessionLifecycleHooks,
type SessionLifecycleHookSlots,
} from '#/session/sessionLifecycleHooks/sessionLifecycleHooks';
import { ISessionIndexMirror } from '#/app/sessionIndex/sessionIndex';
import {
ISessionMetadata,
@ -521,19 +516,19 @@ class RecordingSessionExternalHooksService
constructor(
@ISessionContext private readonly context: ISessionContext,
@ISessionLifecycleHooks hooks: Hooks<SessionLifecycleHookSlots>,
@ISessionLifecycleService lifecycle: ISessionLifecycleService,
) {
super();
this._register(
hooks.onDidCreateSession.register('test', async (event, next) => {
lifecycle.onDidCreateSession((event) => {
if (event.sessionId !== this.context.sessionId) return;
recordedSessionHookEvents.push(`create:${event.source}:${this.context.sessionId}`);
await next();
}),
);
this._register(
hooks.onWillCloseSession.register('test', async (event, next) => {
lifecycle.onWillCloseSession((event) => {
if (event.sessionId !== this.context.sessionId) return;
recordedSessionHookEvents.push(`close:${event.reason}:${this.context.sessionId}`);
await next();
}),
);
}

View file

@ -34,7 +34,6 @@ import {
drainSessionIndexMirror,
HostProcessError,
IHostRequestHeaders,
ISessionLifecycleHooks,
ISessionLifecycleService,
IWorkspaceLifecycleService,
OsProcessErrors,
@ -442,13 +441,11 @@ key = "${titleOAuthRef.key}"
const closeGate = new Promise<void>((resolve) => {
openCloseGate = resolve;
});
tempHandle!.accessor
.get(ISessionLifecycleHooks)
.onWillCloseSession.register('test-block', async (_event, next) => {
markCloseStarted();
await closeGate;
await next();
});
handler.accessor.get(ISessionLifecycleService).onWillCloseSession((event) => {
if (event.sessionId !== 'ses_title_race') return;
markCloseStarted();
event.waitUntil(closeGate);
});
resolveFetch(
new Response(JSON.stringify({ title: 'Generated title' }), {