perf(agents): remove spawn hook announce import tax

This commit is contained in:
Peter Steinberger
2026-04-07 07:13:56 +01:00
parent e16a64ba1a
commit e8817dde8e
6 changed files with 243 additions and 204 deletions

View File

@@ -13,11 +13,11 @@ import {
setupSessionsSpawnGatewayMock,
setSessionsSpawnConfigOverride,
} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js";
import { resolveRequesterStoreKey } from "./subagent-announce-delivery.js";
import {
getLatestSubagentRunByChildSessionKey,
resetSubagentRegistryForTests,
} from "./subagent-registry.js";
import { resolveRequesterStoreKey } from "./subagent-requester-store-key.js";
const fastModeEnv = vi.hoisted(() => {
const previous = process.env.OPENCLAW_TEST_FAST;

View File

@@ -1,20 +1,14 @@
import { vi, type Mock } from "vitest";
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import {
__testing as subagentAnnounceDeliveryTesting,
resolveRequesterStoreKey,
} from "./subagent-announce-delivery.js";
import { __testing as subagentAnnounceOutputTesting } from "./subagent-announce-output.js";
import {
__testing as subagentAnnounceTesting,
captureSubagentCompletionReply,
runSubagentAnnounceFlow,
} from "./subagent-announce.js";
import { __testing as subagentRegistryTesting } from "./subagent-registry.js";
import { resolveRequesterStoreKey } from "./subagent-requester-store-key.js";
import { __testing as subagentSpawnTesting } from "./subagent-spawn.js";
type SessionsSpawnTestConfig = ReturnType<(typeof import("../config/config.js"))["loadConfig"]>;
type SessionsSpawnHookRunner = SubagentLifecycleHookRunner | null;
type CaptureSubagentCompletionReply =
(typeof import("./subagent-announce.js"))["captureSubagentCompletionReply"];
type RunSubagentAnnounceFlow = (typeof import("./subagent-announce.js"))["runSubagentAnnounceFlow"];
type CreateSessionsSpawnTool =
(typeof import("./tools/sessions-spawn-tool.js"))["createSessionsSpawnTool"];
export type CreateOpenClawToolsOpts = Parameters<CreateSessionsSpawnTool>[0];
@@ -39,7 +33,7 @@ const hoisted = vi.hoisted(() => {
},
} as SessionsSpawnTestConfig;
let configOverride = defaultConfigOverride;
const defaultRunSubagentAnnounceFlow: typeof runSubagentAnnounceFlow = async (params) => {
const defaultRunSubagentAnnounceFlow: RunSubagentAnnounceFlow = async (params) => {
const statusLabel =
params.outcome?.status === "timeout" ? "timed out" : "completed successfully";
const requesterSessionKey = resolveRequesterStoreKey(
@@ -79,6 +73,8 @@ const hoisted = vi.hoisted(() => {
return true;
};
const defaultCaptureSubagentCompletionReply: CaptureSubagentCompletionReply = async () =>
undefined;
const state = {
get configOverride() {
return configOverride;
@@ -87,6 +83,8 @@ const hoisted = vi.hoisted(() => {
configOverride = next;
},
hookRunnerOverride: null as SessionsSpawnHookRunner,
defaultCaptureSubagentCompletionReply,
captureSubagentCompletionReplyOverride: defaultCaptureSubagentCompletionReply,
defaultRunSubagentAnnounceFlow,
runSubagentAnnounceFlowOverride: defaultRunSubagentAnnounceFlow,
};
@@ -131,7 +129,7 @@ export function setSessionsSpawnHookRunnerOverride(next: SessionsSpawnHookRunner
hoisted.state.hookRunnerOverride = next;
}
export function setSessionsSpawnAnnounceFlowOverride(next: typeof runSubagentAnnounceFlow): void {
export function setSessionsSpawnAnnounceFlowOverride(next: RunSubagentAnnounceFlow): void {
hoisted.state.runSubagentAnnounceFlowOverride = next;
}
@@ -142,22 +140,11 @@ export async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) {
loadConfig: () => hoisted.state.configOverride,
updateSessionStore: async (_storePath, mutator) => mutator({}),
});
subagentAnnounceTesting.setDepsForTest({
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
loadConfig: () => hoisted.state.configOverride,
});
subagentAnnounceDeliveryTesting.setDepsForTest({
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
loadConfig: () => hoisted.state.configOverride,
});
subagentAnnounceOutputTesting.setDepsForTest({
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
loadConfig: () => hoisted.state.configOverride,
});
subagentRegistryTesting.setDepsForTest({
callGateway: (optsUnknown) => hoisted.callGatewayMock(optsUnknown),
loadConfig: () => hoisted.state.configOverride,
captureSubagentCompletionReply,
captureSubagentCompletionReply: (sessionKey) =>
hoisted.state.captureSubagentCompletionReplyOverride(sessionKey),
runSubagentAnnounceFlow: (params) => hoisted.state.runSubagentAnnounceFlowOverride(params),
});
if (!cachedCreateSessionsSpawnTool) {

View File

@@ -1,15 +1,32 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "./test-helpers/fast-core-tools.js";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
findGatewayRequest,
getCallGatewayMock,
getGatewayMethods,
getSessionsSpawnTool,
resetSessionsSpawnHookRunnerOverride,
setSessionsSpawnConfigOverride,
setSessionsSpawnHookRunnerOverride,
} from "./openclaw-tools.subagents.sessions-spawn.test-harness.js";
import { resetSubagentRegistryForTests } from "./subagent-registry.js";
createSubagentSpawnTestConfig,
loadSubagentSpawnModuleForTest,
} from "./subagent-spawn.test-helpers.js";
type GatewayRequest = { method?: string; params?: Record<string, unknown> };
const hoisted = vi.hoisted(() => ({
callGatewayMock: vi.fn(),
configOverride: {
session: { mainKey: "main", scope: "per-sender" },
tools: {
sessions_spawn: {
attachments: {
enabled: true,
maxFiles: 50,
maxFileBytes: 1 * 1024 * 1024,
maxTotalBytes: 5 * 1024 * 1024,
},
},
},
agents: {
defaults: {
workspace: "/tmp",
},
},
},
}));
const hookRunnerMocks = vi.hoisted(() => ({
hasSubagentEndedHook: true,
@@ -38,6 +55,58 @@ const hookRunnerMocks = vi.hoisted(() => ({
runSubagentEnded: vi.fn(async () => {}),
}));
let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests;
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
function getGatewayRequests(): GatewayRequest[] {
return hoisted.callGatewayMock.mock.calls.map((call) => call[0] as GatewayRequest);
}
function getGatewayMethods() {
return getGatewayRequests().map((request) => request.method);
}
function findGatewayRequest(method: string): GatewayRequest | undefined {
return getGatewayRequests().find((request) => request.method === method);
}
function setConfig(next: Record<string, unknown>) {
hoisted.configOverride = createSubagentSpawnTestConfig(undefined, next);
}
async function spawn(params?: {
toolCallId?: string;
task?: string;
label?: string;
runTimeoutSeconds?: number;
thread?: boolean;
mode?: "run" | "session";
agentSessionKey?: string;
agentChannel?: string;
agentAccountId?: string;
agentTo?: string;
agentThreadId?: string | number;
}) {
return await spawnSubagentDirect(
{
task: params?.task ?? "do thing",
...(params?.label ? { label: params.label } : {}),
...(typeof params?.runTimeoutSeconds === "number"
? { runTimeoutSeconds: params.runTimeoutSeconds }
: {}),
...(params?.thread ? { thread: true } : {}),
...(params?.mode ? { mode: params.mode } : {}),
},
{
agentSessionKey: params?.agentSessionKey ?? "main",
agentChannel: params?.agentChannel ?? "discord",
agentAccountId: params?.agentAccountId,
agentTo: params?.agentTo,
agentThreadId: params?.agentThreadId,
},
);
}
function expectSessionsDeleteWithoutAgentStart() {
const methods = getGatewayMethods();
expect(methods).toContain("sessions.delete");
@@ -45,8 +114,7 @@ function expectSessionsDeleteWithoutAgentStart() {
}
function mockAgentStartFailure() {
const callGatewayMock = getCallGatewayMock();
callGatewayMock.mockImplementation(async (opts: unknown) => {
hoisted.callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "agent") {
throw new Error("spawn failed");
@@ -55,47 +123,6 @@ function mockAgentStartFailure() {
});
}
async function runSessionThreadSpawnAndGetError(params: {
toolCallId: string;
spawningResult: { status: "error"; error: string } | { status: "ok"; threadBindingReady: false };
}): Promise<{ error?: string; childSessionKey?: string }> {
hookRunnerMocks.runSubagentSpawning.mockResolvedValueOnce(params.spawningResult);
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "discord",
agentAccountId: "work",
agentTo: "channel:123",
});
const result = await tool.execute(params.toolCallId, {
task: "do thing",
runTimeoutSeconds: 1,
thread: true,
mode: "session",
});
expect(result.details).toMatchObject({ status: "error" });
return result.details as { error?: string; childSessionKey?: string };
}
async function getDiscordThreadSessionTool() {
return await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "discord",
agentAccountId: "work",
agentTo: "channel:123",
agentThreadId: "456",
});
}
async function executeDiscordThreadSessionSpawn(toolCallId: string) {
const tool = await getDiscordThreadSessionTool();
return await tool.execute(toolCallId, {
task: "do thing",
thread: true,
mode: "session",
});
}
function getSpawnedEventCall(): Record<string, unknown> {
const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [
Record<string, unknown>,
@@ -103,35 +130,33 @@ function getSpawnedEventCall(): Record<string, unknown> {
return event;
}
function expectErrorResultMessage(result: { details: unknown }, pattern: RegExp): void {
expect(result.details).toMatchObject({ status: "error" });
const details = result.details as { error?: string };
expect(details.error).toMatch(pattern);
function expectErrorResultMessage(
result: { error?: string; status: string },
pattern: RegExp,
): void {
expect(result.status).toBe("error");
expect(result.error).toMatch(pattern);
}
function expectThreadBindFailureCleanup(
details: { childSessionKey?: string; error?: string },
result: { childSessionKey?: string; error?: string },
pattern: RegExp,
): void {
expect(details.error).toMatch(pattern);
expect(result.error).toMatch(pattern);
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
expectSessionsDeleteWithoutAgentStart();
const deleteCall = findGatewayRequest("sessions.delete");
expect(deleteCall?.params).toMatchObject({
key: details.childSessionKey,
key: result.childSessionKey,
emitLifecycleHooks: false,
});
}
describe("sessions_spawn subagent lifecycle hooks", () => {
beforeEach(() => {
resetSubagentRegistryForTests();
resetSessionsSpawnHookRunnerOverride();
hookRunnerMocks.hasSubagentEndedHook = true;
hookRunnerMocks.runSubagentSpawning.mockClear();
hookRunnerMocks.runSubagentSpawned.mockClear();
hookRunnerMocks.runSubagentEnded.mockClear();
setSessionsSpawnHookRunnerOverride({
beforeAll(async () => {
({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
callGatewayMock: hoisted.callGatewayMock,
loadConfig: () => hoisted.configOverride,
hookRunner: {
hasHooks: (hookName: string) =>
hookName === "subagent_spawning" ||
hookName === "subagent_spawned" ||
@@ -139,22 +164,36 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
runSubagentSpawning: hookRunnerMocks.runSubagentSpawning,
runSubagentSpawned: hookRunnerMocks.runSubagentSpawned,
runSubagentEnded: hookRunnerMocks.runSubagentEnded,
});
const callGatewayMock = getCallGatewayMock();
callGatewayMock.mockClear();
setSessionsSpawnConfigOverride({
},
resetModules: false,
sessionStorePath: "/tmp/subagent-spawn-hooks-session-store.json",
}));
});
describe("sessions_spawn subagent lifecycle hooks", () => {
beforeEach(() => {
resetSubagentRegistryForTests();
hoisted.callGatewayMock.mockReset();
hookRunnerMocks.hasSubagentEndedHook = true;
hookRunnerMocks.runSubagentSpawning.mockClear();
hookRunnerMocks.runSubagentSpawned.mockClear();
hookRunnerMocks.runSubagentEnded.mockClear();
setConfig({
session: {
mainKey: "main",
scope: "per-sender",
},
});
callGatewayMock.mockImplementation(async (opts: unknown) => {
hoisted.callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "agent") {
return { runId: "run-1", status: "accepted", acceptedAt: 1 };
if (request.method === "sessions.patch") {
return { ok: true };
}
if (request.method === "agent.wait") {
return { runId: "run-1", status: "running" };
if (request.method === "sessions.delete") {
return { ok: true };
}
if (request.method === "agent") {
return { runId: "run-1", status: "accepted", acceptedAt: 1_001 };
}
return {};
});
@@ -165,22 +204,16 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
});
it("runs subagent_spawning and emits subagent_spawned with requester metadata", async () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "discord",
const result = await spawn({
label: "research",
runTimeoutSeconds: 1,
thread: true,
agentAccountId: "work",
agentTo: "channel:123",
agentThreadId: 456,
});
const result = await tool.execute("call", {
task: "do thing",
label: "research",
runTimeoutSeconds: 1,
thread: true,
});
expect(result.details).toMatchObject({ status: "accepted", runId: "run-1" });
expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledWith(
{
@@ -229,18 +262,12 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
});
it("emits subagent_spawned with threadRequested=false when not requested", async () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "discord",
const result = await spawn({
runTimeoutSeconds: 1,
agentTo: "channel:123",
});
const result = await tool.execute("call2", {
task: "do thing",
runTimeoutSeconds: 1,
});
expect(result.details).toMatchObject({ status: "accepted", runId: "run-1" });
expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled();
expect(hookRunnerMocks.runSubagentSpawned).toHaveBeenCalledTimes(1);
const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [
@@ -257,20 +284,14 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
});
it("respects explicit mode=run when thread binding is requested", async () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "discord",
agentTo: "channel:123",
});
const result = await tool.execute("call3", {
task: "do thing",
const result = await spawn({
runTimeoutSeconds: 1,
thread: true,
mode: "run",
agentTo: "channel:123",
});
expect(result.details).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" });
expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" });
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
const event = getSpawnedEventCall();
expect(event).toMatchObject({
@@ -280,57 +301,57 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
});
it("returns error when thread binding cannot be created", async () => {
const details = await runSessionThreadSpawnAndGetError({
hookRunnerMocks.runSubagentSpawning.mockResolvedValueOnce({
status: "error",
error: "Unable to create or bind a Discord thread for this subagent session.",
});
const result = await spawn({
toolCallId: "call4",
spawningResult: {
status: "error",
error: "Unable to create or bind a Discord thread for this subagent session.",
},
});
expectThreadBindFailureCleanup(details, /thread/i);
});
it("returns error when thread binding is not marked ready", async () => {
const details = await runSessionThreadSpawnAndGetError({
toolCallId: "call4b",
spawningResult: {
status: "ok",
threadBindingReady: false,
},
});
expectThreadBindFailureCleanup(details, /unable to create or bind a thread/i);
});
it("rejects mode=session when thread=true is not requested", async () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "discord",
runTimeoutSeconds: 1,
thread: true,
mode: "session",
agentAccountId: "work",
agentTo: "channel:123",
});
const result = await tool.execute("call6", {
task: "do thing",
expectThreadBindFailureCleanup(result, /thread/i);
});
it("returns error when thread binding is not marked ready", async () => {
hookRunnerMocks.runSubagentSpawning.mockResolvedValueOnce({
status: "ok",
threadBindingReady: false,
});
const result = await spawn({
toolCallId: "call4b",
runTimeoutSeconds: 1,
thread: true,
mode: "session",
agentAccountId: "work",
agentTo: "channel:123",
});
expectThreadBindFailureCleanup(result, /unable to create or bind a thread/i);
});
it("rejects mode=session when thread=true is not requested", async () => {
const result = await spawn({
mode: "session",
agentTo: "channel:123",
});
expectErrorResultMessage(result, /requires thread=true/i);
expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled();
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
const callGatewayMock = getCallGatewayMock();
expect(callGatewayMock).not.toHaveBeenCalled();
expect(hoisted.callGatewayMock).not.toHaveBeenCalled();
});
it("rejects thread=true on channels without thread support", async () => {
const tool = await getSessionsSpawnTool({
agentSessionKey: "main",
agentChannel: "signal",
agentTo: "+123",
});
const result = await tool.execute("call5", {
task: "do thing",
const result = await spawn({
thread: true,
mode: "session",
agentChannel: "signal",
agentTo: "+123",
});
expectErrorResultMessage(result, /only discord/i);
@@ -341,9 +362,15 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
it("runs subagent_ended cleanup hook when agent start fails after successful bind", async () => {
mockAgentStartFailure();
const result = await executeDiscordThreadSessionSpawn("call7");
const result = await spawn({
thread: true,
mode: "session",
agentAccountId: "work",
agentTo: "channel:123",
agentThreadId: "456",
});
expect(result.details).toMatchObject({ status: "error" });
expect(result).toMatchObject({ status: "error" });
expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1);
const [event] = (hookRunnerMocks.runSubagentEnded.mock.calls[0] ?? []) as unknown as [
Record<string, unknown>,
@@ -368,9 +395,15 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => {
hookRunnerMocks.hasSubagentEndedHook = false;
mockAgentStartFailure();
const result = await executeDiscordThreadSessionSpawn("call8");
const result = await spawn({
thread: true,
mode: "session",
agentAccountId: "work",
agentTo: "channel:123",
agentThreadId: "456",
});
expect(result.details).toMatchObject({ status: "error" });
expect(result).toMatchObject({ status: "error" });
expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled();
const methods = getGatewayMethods();
expect(methods).toContain("sessions.delete");
@@ -382,8 +415,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
});
it("cleans up the provisional session when lineage patching fails after thread binding", async () => {
const callGatewayMock = getCallGatewayMock();
callGatewayMock.mockImplementation(async (opts: unknown) => {
hoisted.callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: Record<string, unknown> };
if (request.method === "sessions.patch" && typeof request.params?.spawnedBy === "string") {
throw new Error("lineage patch failed");
@@ -391,12 +423,21 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
if (request.method === "sessions.delete") {
return { ok: true };
}
if (request.method === "agent") {
return { runId: "run-1", status: "accepted", acceptedAt: 1_001 };
}
return {};
});
const result = await executeDiscordThreadSessionSpawn("call9");
const result = await spawn({
thread: true,
mode: "session",
agentAccountId: "work",
agentTo: "channel:123",
agentThreadId: "456",
});
expect(result.details).toMatchObject({
expect(result).toMatchObject({
status: "error",
error: "lineage patch failed",
});
@@ -407,7 +448,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
expect(methods).not.toContain("agent");
const deleteCall = findGatewayRequest("sessions.delete");
expect(deleteCall?.params).toMatchObject({
key: (result.details as { childSessionKey?: string }).childSessionKey,
key: result.childSessionKey,
deleteTranscript: true,
emitLifecycleHooks: true,
});

View File

@@ -1,5 +1,5 @@
import type { ConversationRef } from "../infra/outbound/session-binding-service.js";
import { normalizeAccountId, normalizeMainKey } from "../routing/session-key.js";
import { normalizeAccountId } from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import { isCronSessionKey } from "../sessions/session-key-utils.js";
import {
@@ -26,7 +26,6 @@ import {
resolveAgentIdFromSessionKey,
resolveConversationIdFromTargets,
resolveExternalBestEffortDeliveryTarget,
resolveMainSessionKey,
resolveQueueSettings,
resolveStorePath,
} from "./subagent-announce-delivery.runtime.js";
@@ -37,6 +36,7 @@ import {
import { resolveAnnounceOrigin, type DeliveryContext } from "./subagent-announce-origin.js";
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import { resolveRequesterStoreKey } from "./subagent-requester-store-key.js";
import type { SpawnSubagentMode } from "./subagent-spawn.js";
export { resolveAnnounceOrigin } from "./subagent-announce-origin.js";
@@ -302,28 +302,6 @@ async function sendAnnounce(item: AnnounceQueueItem) {
});
}
export function resolveRequesterStoreKey(
cfg: ReturnType<typeof loadConfig>,
requesterSessionKey: string,
): string {
const raw = (requesterSessionKey ?? "").trim();
if (!raw) {
return raw;
}
if (raw === "global" || raw === "unknown") {
return raw;
}
if (raw.startsWith("agent:")) {
return raw;
}
const mainKey = normalizeMainKey(cfg.session?.mainKey);
if (raw === "main" || raw === mainKey) {
return resolveMainSessionKey(cfg);
}
const agentId = resolveAgentIdFromSessionKey(raw);
return `agent:${agentId}:${raw}`;
}
export function loadRequesterSessionEntry(requesterSessionKey: string) {
const cfg = subagentAnnounceDeliveryDeps.loadConfig();
const canonicalKey = resolveRequesterStoreKey(cfg, requesterSessionKey);

View File

@@ -0,0 +1,32 @@
import {
resolveAgentIdFromSessionKey,
resolveMainSessionKey,
} from "../config/sessions/main-session.js";
import { normalizeMainKey } from "../routing/session-key.js";
type RequesterStoreKeyConfig = {
session?: { mainKey?: string };
agents?: { list?: Array<{ id?: string; default?: boolean }> };
};
export function resolveRequesterStoreKey(
cfg: RequesterStoreKeyConfig | undefined,
requesterSessionKey: string,
): string {
const raw = (requesterSessionKey ?? "").trim();
if (!raw) {
return raw;
}
if (raw === "global" || raw === "unknown") {
return raw;
}
if (raw.startsWith("agent:")) {
return raw;
}
const mainKey = normalizeMainKey(cfg?.session?.mainKey);
if (raw === "main" || raw === mainKey) {
return resolveMainSessionKey(cfg);
}
const agentId = resolveAgentIdFromSessionKey(raw);
return `agent:${agentId}:${raw}`;
}

View File

@@ -8,7 +8,8 @@ type MockImplementationTarget = {
};
type SessionStore = Record<string, Record<string, unknown>>;
type SessionStoreMutator = (store: SessionStore) => unknown;
type HookRunner = Pick<SubagentLifecycleHookRunner, "hasHooks" | "runSubagentSpawning">;
type HookRunner = Pick<SubagentLifecycleHookRunner, "hasHooks" | "runSubagentSpawning"> &
Partial<Pick<SubagentLifecycleHookRunner, "runSubagentSpawned" | "runSubagentEnded">>;
type SubagentSpawnModuleForTest = Awaited<typeof import("./subagent-spawn.js")> & {
resetSubagentRegistryForTests: MockFn;
};