fix: resolve session ids in session tools
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.clawd.bot
|
||||
- TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
|
||||
|
||||
### Fixes
|
||||
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
||||
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
||||
- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
|
||||
- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
|
||||
|
||||
@@ -56,19 +56,20 @@ Row shape (JSON):
|
||||
Fetch transcript for one session.
|
||||
|
||||
Parameters:
|
||||
- `sessionKey` (required)
|
||||
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
|
||||
- `limit?: number` max messages (server clamps)
|
||||
- `includeTools?: boolean` (default false)
|
||||
|
||||
Behavior:
|
||||
- `includeTools=false` filters `role: "toolResult"` messages.
|
||||
- Returns messages array in the raw transcript format.
|
||||
- When given a `sessionId`, Clawdbot resolves it to the corresponding session key (missing ids error).
|
||||
|
||||
## sessions_send
|
||||
Send a message into another session.
|
||||
|
||||
Parameters:
|
||||
- `sessionKey` (required)
|
||||
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
|
||||
- `message` (required)
|
||||
- `timeoutSeconds?: number` (default >0; 0 = fire-and-forget)
|
||||
|
||||
|
||||
@@ -379,10 +379,10 @@ List sessions, inspect transcript history, or send to another session.
|
||||
|
||||
Core parameters:
|
||||
- `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none)
|
||||
- `sessions_history`: `sessionKey`, `limit?`, `includeTools?`
|
||||
- `sessions_send`: `sessionKey`, `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
||||
- `sessions_history`: `sessionKey` (or `sessionId`), `limit?`, `includeTools?`
|
||||
- `sessions_send`: `sessionKey` (or `sessionId`), `message`, `timeoutSeconds?` (0 = fire-and-forget)
|
||||
- `sessions_spawn`: `task`, `label?`, `agentId?`, `model?`, `runTimeoutSeconds?`, `cleanup?`
|
||||
- `session_status`: `sessionKey?` (default current), `model?` (`default` clears override)
|
||||
- `session_status`: `sessionKey?` (default current; accepts `sessionId`), `model?` (`default` clears override)
|
||||
|
||||
Notes:
|
||||
- `main` is the canonical direct-chat key; global/unknown are hidden.
|
||||
|
||||
@@ -17,7 +17,8 @@ vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||
updateSessionStoreMock(storePath, store);
|
||||
return store;
|
||||
},
|
||||
resolveStorePath: () => "/tmp/sessions.json",
|
||||
resolveStorePath: (_store: string | undefined, opts?: { agentId?: string }) =>
|
||||
opts?.agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -117,11 +118,118 @@ describe("session_status tool", () => {
|
||||
if (!tool) throw new Error("missing session_status tool");
|
||||
|
||||
await expect(tool.execute("call2", { sessionKey: "nope" })).rejects.toThrow(
|
||||
"Unknown sessionKey",
|
||||
"Unknown sessionId",
|
||||
);
|
||||
expect(updateSessionStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves sessionId inputs", async () => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
updateSessionStoreMock.mockReset();
|
||||
const sessionId = "sess-main";
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"agent:main:main": {
|
||||
sessionId,
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = createClawdbotTools({ agentSessionKey: "main" }).find(
|
||||
(candidate) => candidate.name === "session_status",
|
||||
);
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing session_status tool");
|
||||
|
||||
const result = await tool.execute("call3", { sessionKey: sessionId });
|
||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||
expect(details.ok).toBe(true);
|
||||
expect(details.sessionKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("uses non-standard session keys without sessionId resolution", async () => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
updateSessionStoreMock.mockReset();
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"temp:slug-generator": {
|
||||
sessionId: "sess-temp",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = createClawdbotTools({ agentSessionKey: "main" }).find(
|
||||
(candidate) => candidate.name === "session_status",
|
||||
);
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing session_status tool");
|
||||
|
||||
const result = await tool.execute("call4", { sessionKey: "temp:slug-generator" });
|
||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||
expect(details.ok).toBe(true);
|
||||
expect(details.sessionKey).toBe("temp:slug-generator");
|
||||
});
|
||||
|
||||
it("blocks cross-agent session_status without agent-to-agent access", async () => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
updateSessionStoreMock.mockReset();
|
||||
loadSessionStoreMock.mockReturnValue({
|
||||
"agent:other:main": {
|
||||
sessionId: "s2",
|
||||
updatedAt: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const tool = createClawdbotTools({ agentSessionKey: "agent:main:main" }).find(
|
||||
(candidate) => candidate.name === "session_status",
|
||||
);
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing session_status tool");
|
||||
|
||||
await expect(tool.execute("call5", { sessionKey: "agent:other:main" })).rejects.toThrow(
|
||||
"Agent-to-agent status is disabled",
|
||||
);
|
||||
});
|
||||
|
||||
it("scopes bare session keys to the requester agent", async () => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
updateSessionStoreMock.mockReset();
|
||||
const stores = new Map<string, Record<string, unknown>>([
|
||||
[
|
||||
"/tmp/main/sessions.json",
|
||||
{
|
||||
"agent:main:main": { sessionId: "s-main", updatedAt: 10 },
|
||||
},
|
||||
],
|
||||
[
|
||||
"/tmp/support/sessions.json",
|
||||
{
|
||||
main: { sessionId: "s-support", updatedAt: 20 },
|
||||
},
|
||||
],
|
||||
]);
|
||||
loadSessionStoreMock.mockImplementation((storePath: string) => {
|
||||
return stores.get(storePath) ?? {};
|
||||
});
|
||||
updateSessionStoreMock.mockImplementation(
|
||||
(_storePath: string, store: Record<string, unknown>) => {
|
||||
// Keep map in sync for resolveSessionEntry fallbacks if needed.
|
||||
if (_storePath) {
|
||||
stores.set(_storePath, store);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const tool = createClawdbotTools({ agentSessionKey: "agent:support:main" }).find(
|
||||
(candidate) => candidate.name === "session_status",
|
||||
);
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing session_status tool");
|
||||
|
||||
const result = await tool.execute("call6", { sessionKey: "main" });
|
||||
const details = result.details as { ok?: boolean; sessionKey?: string };
|
||||
expect(details.ok).toBe(true);
|
||||
expect(details.sessionKey).toBe("main");
|
||||
});
|
||||
|
||||
it("resets per-session model override via model=default", async () => {
|
||||
loadSessionStoreMock.mockReset();
|
||||
updateSessionStoreMock.mockReset();
|
||||
|
||||
@@ -172,6 +172,62 @@ describe("sessions tools", () => {
|
||||
expect(withToolsDetails.messages).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("sessions_history resolves sessionId inputs", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
const sessionId = "sess-group";
|
||||
const targetKey = "agent:main:discord:channel:1457165743010611293";
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string; params?: Record<string, unknown> };
|
||||
if (request.method === "sessions.resolve") {
|
||||
return {
|
||||
key: targetKey,
|
||||
};
|
||||
}
|
||||
if (request.method === "chat.history") {
|
||||
return {
|
||||
messages: [{ role: "assistant", content: [{ type: "text", text: "ok" }] }],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing sessions_history tool");
|
||||
|
||||
const result = await tool.execute("call5", { sessionKey: sessionId });
|
||||
const details = result.details as { messages?: unknown[] };
|
||||
expect(details.messages).toHaveLength(1);
|
||||
const historyCall = callGatewayMock.mock.calls.find(
|
||||
(call) => (call[0] as { method?: string }).method === "chat.history",
|
||||
);
|
||||
expect(historyCall?.[0]).toMatchObject({
|
||||
method: "chat.history",
|
||||
params: { sessionKey: targetKey },
|
||||
});
|
||||
});
|
||||
|
||||
it("sessions_history errors on missing sessionId", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
const sessionId = "aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa";
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string };
|
||||
if (request.method === "sessions.resolve") {
|
||||
throw new Error("No session found");
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing sessions_history tool");
|
||||
|
||||
const result = await tool.execute("call6", { sessionKey: sessionId });
|
||||
const details = result.details as { status?: string; error?: string };
|
||||
expect(details.status).toBe("error");
|
||||
expect(details.error).toMatch(/Session not found|No session found/);
|
||||
});
|
||||
|
||||
it("sessions_send supports fire-and-forget and wait", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
@@ -313,6 +369,50 @@ describe("sessions tools", () => {
|
||||
expect(sendCallCount).toBe(0);
|
||||
});
|
||||
|
||||
it("sessions_send resolves sessionId inputs", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
const sessionId = "sess-send";
|
||||
const targetKey = "agent:main:discord:channel:123";
|
||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||
const request = opts as { method?: string; params?: Record<string, unknown> };
|
||||
if (request.method === "sessions.resolve") {
|
||||
return { key: targetKey };
|
||||
}
|
||||
if (request.method === "agent") {
|
||||
return { runId: "run-1", acceptedAt: 123 };
|
||||
}
|
||||
if (request.method === "agent.wait") {
|
||||
return { status: "ok" };
|
||||
}
|
||||
if (request.method === "chat.history") {
|
||||
return { messages: [] };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const tool = createClawdbotTools({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "discord",
|
||||
}).find((candidate) => candidate.name === "sessions_send");
|
||||
expect(tool).toBeDefined();
|
||||
if (!tool) throw new Error("missing sessions_send tool");
|
||||
|
||||
const result = await tool.execute("call7", {
|
||||
sessionKey: sessionId,
|
||||
message: "ping",
|
||||
timeoutSeconds: 0,
|
||||
});
|
||||
const details = result.details as { status?: string };
|
||||
expect(details.status).toBe("accepted");
|
||||
const agentCall = callGatewayMock.mock.calls.find(
|
||||
(call) => (call[0] as { method?: string }).method === "agent",
|
||||
);
|
||||
expect(agentCall?.[0]).toMatchObject({
|
||||
method: "agent",
|
||||
params: { sessionKey: targetKey },
|
||||
});
|
||||
});
|
||||
|
||||
it("sessions_send runs ping-pong then announces", async () => {
|
||||
callGatewayMock.mockReset();
|
||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
||||
|
||||
@@ -40,7 +40,13 @@ import {
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { readStringParam } from "./common.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||
import {
|
||||
shouldResolveSessionIdInput,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
createAgentToAgentPolicy,
|
||||
} from "./sessions-helpers.js";
|
||||
import { loadCombinedSessionStoreForGateway } from "../../gateway/session-utils.js";
|
||||
|
||||
const SessionStatusToolSchema = Type.Object({
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
@@ -149,6 +155,22 @@ function resolveSessionEntry(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveSessionKeyFromSessionId(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
sessionId: string;
|
||||
agentId?: string;
|
||||
}): string | null {
|
||||
const trimmed = params.sessionId.trim();
|
||||
if (!trimmed) return null;
|
||||
const { store } = loadCombinedSessionStoreForGateway(params.cfg);
|
||||
const match = Object.entries(store).find(([key, entry]) => {
|
||||
if (entry?.sessionId !== trimmed) return false;
|
||||
if (!params.agentId) return true;
|
||||
return resolveAgentIdFromSessionKey(key) === params.agentId;
|
||||
});
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
async function resolveModelOverride(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
raw: string;
|
||||
@@ -222,24 +244,74 @@ export function createSessionStatusTool(opts?: {
|
||||
const params = args as Record<string, unknown>;
|
||||
const cfg = opts?.config ?? loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
|
||||
const requestedKeyRaw = readStringParam(params, "sessionKey") ?? opts?.agentSessionKey;
|
||||
const requestedKeyParam = readStringParam(params, "sessionKey");
|
||||
let requestedKeyRaw = requestedKeyParam ?? opts?.agentSessionKey;
|
||||
if (!requestedKeyRaw?.trim()) {
|
||||
throw new Error("sessionKey required");
|
||||
}
|
||||
|
||||
const agentId = resolveAgentIdFromSessionKey(opts?.agentSessionKey ?? requestedKeyRaw);
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(
|
||||
opts?.agentSessionKey ?? requestedKeyRaw,
|
||||
);
|
||||
const ensureAgentAccess = (targetAgentId: string) => {
|
||||
if (targetAgentId === requesterAgentId) return;
|
||||
// Gate cross-agent access behind tools.agentToAgent settings.
|
||||
if (!a2aPolicy.enabled) {
|
||||
throw new Error(
|
||||
"Agent-to-agent status is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access.",
|
||||
);
|
||||
}
|
||||
if (!a2aPolicy.isAllowed(requesterAgentId, targetAgentId)) {
|
||||
throw new Error("Agent-to-agent session status denied by tools.agentToAgent.allow.");
|
||||
}
|
||||
};
|
||||
|
||||
const resolved = resolveSessionEntry({
|
||||
if (requestedKeyRaw.startsWith("agent:")) {
|
||||
ensureAgentAccess(resolveAgentIdFromSessionKey(requestedKeyRaw));
|
||||
}
|
||||
|
||||
const isExplicitAgentKey = requestedKeyRaw.startsWith("agent:");
|
||||
let agentId = isExplicitAgentKey
|
||||
? resolveAgentIdFromSessionKey(requestedKeyRaw)
|
||||
: requesterAgentId;
|
||||
let storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
let store = loadSessionStore(storePath);
|
||||
|
||||
// Resolve against the requester-scoped store first to avoid leaking default agent data.
|
||||
let resolved = resolveSessionEntry({
|
||||
store,
|
||||
keyRaw: requestedKeyRaw,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
|
||||
if (!resolved && shouldResolveSessionIdInput(requestedKeyRaw)) {
|
||||
const resolvedKey = resolveSessionKeyFromSessionId({
|
||||
cfg,
|
||||
sessionId: requestedKeyRaw,
|
||||
agentId: a2aPolicy.enabled ? undefined : requesterAgentId,
|
||||
});
|
||||
if (resolvedKey) {
|
||||
// If resolution points at another agent, enforce A2A policy before switching stores.
|
||||
ensureAgentAccess(resolveAgentIdFromSessionKey(resolvedKey));
|
||||
requestedKeyRaw = resolvedKey;
|
||||
agentId = resolveAgentIdFromSessionKey(resolvedKey);
|
||||
storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
store = loadSessionStore(storePath);
|
||||
resolved = resolveSessionEntry({
|
||||
store,
|
||||
keyRaw: requestedKeyRaw,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
throw new Error(`Unknown sessionKey: ${requestedKeyRaw}`);
|
||||
const kind = shouldResolveSessionIdInput(requestedKeyRaw) ? "sessionId" : "sessionKey";
|
||||
throw new Error(`Unknown ${kind}: ${requestedKeyRaw}`);
|
||||
}
|
||||
|
||||
const configured = resolveDefaultModelForAgent({ cfg, agentId });
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { sanitizeUserFacingText } from "../pi-embedded-helpers.js";
|
||||
import {
|
||||
stripDowngradedToolCallText,
|
||||
stripMinimaxToolCallXml,
|
||||
stripThinkingTagsFromText,
|
||||
} from "../pi-embedded-utils.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js";
|
||||
|
||||
export type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other";
|
||||
|
||||
@@ -62,6 +63,195 @@ export function resolveInternalSessionKey(params: { key: string; alias: string;
|
||||
return params.key;
|
||||
}
|
||||
|
||||
export type AgentToAgentPolicy = {
|
||||
enabled: boolean;
|
||||
matchesAllow: (agentId: string) => boolean;
|
||||
isAllowed: (requesterAgentId: string, targetAgentId: string) => boolean;
|
||||
};
|
||||
|
||||
export function createAgentToAgentPolicy(cfg: ClawdbotConfig): AgentToAgentPolicy {
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const enabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
const raw = String(pattern ?? "").trim();
|
||||
if (!raw) return false;
|
||||
if (raw === "*") return true;
|
||||
if (!raw.includes("*")) return raw === agentId;
|
||||
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i");
|
||||
return re.test(agentId);
|
||||
});
|
||||
};
|
||||
const isAllowed = (requesterAgentId: string, targetAgentId: string) => {
|
||||
if (requesterAgentId === targetAgentId) return true;
|
||||
if (!enabled) return false;
|
||||
return matchesAllow(requesterAgentId) && matchesAllow(targetAgentId);
|
||||
};
|
||||
return { enabled, matchesAllow, isAllowed };
|
||||
}
|
||||
|
||||
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
export function looksLikeSessionId(value: string): boolean {
|
||||
return SESSION_ID_RE.test(value.trim());
|
||||
}
|
||||
|
||||
export function looksLikeSessionKey(value: string): boolean {
|
||||
const raw = value.trim();
|
||||
if (!raw) return false;
|
||||
// These are canonical key shapes that should never be treated as sessionIds.
|
||||
if (raw === "main" || raw === "global" || raw === "unknown") return true;
|
||||
if (isAcpSessionKey(raw)) return true;
|
||||
if (raw.startsWith("agent:")) return true;
|
||||
if (raw.startsWith("cron:") || raw.startsWith("hook:")) return true;
|
||||
if (raw.startsWith("node-") || raw.startsWith("node:")) return true;
|
||||
if (raw.includes(":group:") || raw.includes(":channel:")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldResolveSessionIdInput(value: string): boolean {
|
||||
// Treat anything that doesn't look like a well-formed key as a sessionId candidate.
|
||||
return looksLikeSessionId(value) || !looksLikeSessionKey(value);
|
||||
}
|
||||
|
||||
export type SessionReferenceResolution =
|
||||
| {
|
||||
ok: true;
|
||||
key: string;
|
||||
displayKey: string;
|
||||
resolvedViaSessionId: boolean;
|
||||
}
|
||||
| { ok: false; status: "error" | "forbidden"; error: string };
|
||||
|
||||
async function resolveSessionKeyFromSessionId(params: {
|
||||
sessionId: string;
|
||||
alias: string;
|
||||
mainKey: string;
|
||||
requesterInternalKey?: string;
|
||||
restrictToSpawned: boolean;
|
||||
}): Promise<SessionReferenceResolution> {
|
||||
try {
|
||||
// Resolve via gateway so we respect store routing and visibility rules.
|
||||
const result = (await callGateway({
|
||||
method: "sessions.resolve",
|
||||
params: {
|
||||
sessionId: params.sessionId,
|
||||
spawnedBy: params.restrictToSpawned ? params.requesterInternalKey : undefined,
|
||||
includeGlobal: !params.restrictToSpawned,
|
||||
includeUnknown: !params.restrictToSpawned,
|
||||
},
|
||||
})) as { key?: unknown };
|
||||
const key = typeof result?.key === "string" ? result.key.trim() : "";
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
`Session not found: ${params.sessionId} (use the full sessionKey from sessions_list)`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
key,
|
||||
displayKey: resolveDisplaySessionKey({
|
||||
key,
|
||||
alias: params.alias,
|
||||
mainKey: params.mainKey,
|
||||
}),
|
||||
resolvedViaSessionId: true,
|
||||
};
|
||||
} catch (err) {
|
||||
if (params.restrictToSpawned) {
|
||||
return {
|
||||
ok: false,
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${params.sessionId}`,
|
||||
};
|
||||
}
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return {
|
||||
ok: false,
|
||||
status: "error",
|
||||
error:
|
||||
message ||
|
||||
`Session not found: ${params.sessionId} (use the full sessionKey from sessions_list)`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSessionKeyFromKey(params: {
|
||||
key: string;
|
||||
alias: string;
|
||||
mainKey: string;
|
||||
requesterInternalKey?: string;
|
||||
restrictToSpawned: boolean;
|
||||
}): Promise<SessionReferenceResolution | null> {
|
||||
try {
|
||||
// Try key-based resolution first so non-standard keys keep working.
|
||||
const result = (await callGateway({
|
||||
method: "sessions.resolve",
|
||||
params: {
|
||||
key: params.key,
|
||||
spawnedBy: params.restrictToSpawned ? params.requesterInternalKey : undefined,
|
||||
},
|
||||
})) as { key?: unknown };
|
||||
const key = typeof result?.key === "string" ? result.key.trim() : "";
|
||||
if (!key) return null;
|
||||
return {
|
||||
ok: true,
|
||||
key,
|
||||
displayKey: resolveDisplaySessionKey({
|
||||
key,
|
||||
alias: params.alias,
|
||||
mainKey: params.mainKey,
|
||||
}),
|
||||
resolvedViaSessionId: false,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveSessionReference(params: {
|
||||
sessionKey: string;
|
||||
alias: string;
|
||||
mainKey: string;
|
||||
requesterInternalKey?: string;
|
||||
restrictToSpawned: boolean;
|
||||
}): Promise<SessionReferenceResolution> {
|
||||
const raw = params.sessionKey.trim();
|
||||
if (shouldResolveSessionIdInput(raw)) {
|
||||
// Prefer key resolution to avoid misclassifying custom keys as sessionIds.
|
||||
const resolvedByKey = await resolveSessionKeyFromKey({
|
||||
key: raw,
|
||||
alias: params.alias,
|
||||
mainKey: params.mainKey,
|
||||
requesterInternalKey: params.requesterInternalKey,
|
||||
restrictToSpawned: params.restrictToSpawned,
|
||||
});
|
||||
if (resolvedByKey) return resolvedByKey;
|
||||
return await resolveSessionKeyFromSessionId({
|
||||
sessionId: raw,
|
||||
alias: params.alias,
|
||||
mainKey: params.mainKey,
|
||||
requesterInternalKey: params.requesterInternalKey,
|
||||
restrictToSpawned: params.restrictToSpawned,
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: raw,
|
||||
alias: params.alias,
|
||||
mainKey: params.mainKey,
|
||||
});
|
||||
const displayKey = resolveDisplaySessionKey({
|
||||
key: resolvedKey,
|
||||
alias: params.alias,
|
||||
mainKey: params.mainKey,
|
||||
});
|
||||
return { ok: true, key: resolvedKey, displayKey, resolvedViaSessionId: false };
|
||||
}
|
||||
|
||||
export function classifySessionKind(params: {
|
||||
key: string;
|
||||
gatewayKind?: string | null;
|
||||
|
||||
@@ -2,17 +2,14 @@ import { Type } from "@sinclair/typebox";
|
||||
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
isSubagentSessionKey,
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { isSubagentSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readStringParam } from "./common.js";
|
||||
import {
|
||||
resolveDisplaySessionKey,
|
||||
resolveInternalSessionKey,
|
||||
createAgentToAgentPolicy,
|
||||
resolveSessionReference,
|
||||
resolveMainSessionAlias,
|
||||
resolveInternalSessionKey,
|
||||
stripToolMessages,
|
||||
} from "./sessions-helpers.js";
|
||||
|
||||
@@ -58,7 +55,7 @@ export function createSessionsHistoryTool(opts?: {
|
||||
parameters: SessionsHistoryToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
const sessionKey = readStringParam(params, "sessionKey", {
|
||||
const sessionKeyParam = readStringParam(params, "sessionKey", {
|
||||
required: true,
|
||||
});
|
||||
const cfg = loadConfig();
|
||||
@@ -72,17 +69,26 @@ export function createSessionsHistoryTool(opts?: {
|
||||
mainKey,
|
||||
})
|
||||
: undefined;
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
requesterInternalKey &&
|
||||
!!requesterInternalKey &&
|
||||
!isSubagentSessionKey(requesterInternalKey);
|
||||
if (restrictToSpawned) {
|
||||
const resolvedSession = await resolveSessionReference({
|
||||
sessionKey: sessionKeyParam,
|
||||
alias,
|
||||
mainKey,
|
||||
requesterInternalKey,
|
||||
restrictToSpawned,
|
||||
});
|
||||
if (!resolvedSession.ok) {
|
||||
return jsonResult({ status: resolvedSession.status, error: resolvedSession.error });
|
||||
}
|
||||
// From here on, use the canonical key (sessionId inputs already resolved).
|
||||
const resolvedKey = resolvedSession.key;
|
||||
const displayKey = resolvedSession.displayKey;
|
||||
const resolvedViaSessionId = resolvedSession.resolvedViaSessionId;
|
||||
if (restrictToSpawned && !resolvedViaSessionId) {
|
||||
const ok = await isSpawnedSessionAllowed({
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
targetSessionKey: resolvedKey,
|
||||
@@ -90,40 +96,24 @@ export function createSessionsHistoryTool(opts?: {
|
||||
if (!ok) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKeyParam}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
const raw = String(pattern ?? "").trim();
|
||||
if (!raw) return false;
|
||||
if (raw === "*") return true;
|
||||
if (!raw.includes("*")) return raw === agentId;
|
||||
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i");
|
||||
return re.test(agentId);
|
||||
});
|
||||
};
|
||||
const requesterAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||
);
|
||||
const targetAgentId = normalizeAgentId(parseAgentSessionKey(resolvedKey)?.agentId);
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(requesterInternalKey);
|
||||
const targetAgentId = resolveAgentIdFromSessionKey(resolvedKey);
|
||||
const isCrossAgent = requesterAgentId !== targetAgentId;
|
||||
if (isCrossAgent) {
|
||||
if (!a2aEnabled) {
|
||||
if (!a2aPolicy.enabled) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Agent-to-agent history is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access.",
|
||||
});
|
||||
}
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(targetAgentId)) {
|
||||
if (!a2aPolicy.isAllowed(requesterAgentId, targetAgentId)) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: "Agent-to-agent history denied by tools.agentToAgent.allow.",
|
||||
@@ -143,11 +133,7 @@ export function createSessionsHistoryTool(opts?: {
|
||||
const rawMessages = Array.isArray(result?.messages) ? result.messages : [];
|
||||
const messages = includeTools ? rawMessages : stripToolMessages(rawMessages);
|
||||
return jsonResult({
|
||||
sessionKey: resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
}),
|
||||
sessionKey: displayKey,
|
||||
messages,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -4,14 +4,11 @@ import { Type } from "@sinclair/typebox";
|
||||
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
isSubagentSessionKey,
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { isSubagentSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readStringArrayParam } from "./common.js";
|
||||
import {
|
||||
createAgentToAgentPolicy,
|
||||
classifySessionKind,
|
||||
deriveChannel,
|
||||
resolveDisplaySessionKey,
|
||||
@@ -98,24 +95,8 @@ export function createSessionsListTool(opts?: {
|
||||
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
const storePath = typeof list?.path === "string" ? list.path : undefined;
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
const raw = String(pattern ?? "").trim();
|
||||
if (!raw) return false;
|
||||
if (raw === "*") return true;
|
||||
if (!raw.includes("*")) return raw === agentId;
|
||||
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i");
|
||||
return re.test(agentId);
|
||||
});
|
||||
};
|
||||
const requesterAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||
);
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(requesterInternalKey);
|
||||
const rows: SessionListRow[] = [];
|
||||
|
||||
for (const entry of sessions) {
|
||||
@@ -123,12 +104,9 @@ export function createSessionsListTool(opts?: {
|
||||
const key = typeof entry.key === "string" ? entry.key : "";
|
||||
if (!key) continue;
|
||||
|
||||
const entryAgentId = normalizeAgentId(parseAgentSessionKey(key)?.agentId);
|
||||
const entryAgentId = resolveAgentIdFromSessionKey(key);
|
||||
const crossAgent = entryAgentId !== requesterAgentId;
|
||||
if (crossAgent) {
|
||||
if (!a2aEnabled) continue;
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(entryAgentId)) continue;
|
||||
}
|
||||
if (crossAgent && !a2aPolicy.isAllowed(requesterAgentId, entryAgentId)) continue;
|
||||
|
||||
if (key === "unknown") continue;
|
||||
if (key === "global" && alias !== "global") continue;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
isSubagentSessionKey,
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
|
||||
import {
|
||||
@@ -18,10 +18,11 @@ import { AGENT_LANE_NESTED } from "../lanes.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult, readStringParam } from "./common.js";
|
||||
import {
|
||||
createAgentToAgentPolicy,
|
||||
extractAssistantText,
|
||||
resolveDisplaySessionKey,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
resolveSessionReference,
|
||||
stripToolMessages,
|
||||
} from "./sessions-helpers.js";
|
||||
import { buildAgentToAgentMessageContext, resolvePingPongTurns } from "./sessions-send-helpers.js";
|
||||
@@ -63,24 +64,10 @@ export function createSessionsSendTool(opts?: {
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
requesterInternalKey &&
|
||||
!!requesterInternalKey &&
|
||||
!isSubagentSessionKey(requesterInternalKey);
|
||||
|
||||
const routingA2A = cfg.tools?.agentToAgent;
|
||||
const a2aEnabled = routingA2A?.enabled === true;
|
||||
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
|
||||
const matchesAllow = (agentId: string) => {
|
||||
if (allowPatterns.length === 0) return true;
|
||||
return allowPatterns.some((pattern) => {
|
||||
const raw = String(pattern ?? "").trim();
|
||||
if (!raw) return false;
|
||||
if (raw === "*") return true;
|
||||
if (!raw.includes("*")) return raw === agentId;
|
||||
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i");
|
||||
return re.test(agentId);
|
||||
});
|
||||
};
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
|
||||
const sessionKeyParam = readStringParam(params, "sessionKey");
|
||||
const labelParam = readStringParam(params, "label")?.trim() || undefined;
|
||||
@@ -105,7 +92,7 @@ export function createSessionsSendTool(opts?: {
|
||||
let sessionKey = sessionKeyParam;
|
||||
if (!sessionKey && labelParam) {
|
||||
const requesterAgentId = requesterInternalKey
|
||||
? normalizeAgentId(parseAgentSessionKey(requesterInternalKey)?.agentId)
|
||||
? resolveAgentIdFromSessionKey(requesterInternalKey)
|
||||
: undefined;
|
||||
const requestedAgentId = labelAgentIdParam
|
||||
? normalizeAgentId(labelAgentIdParam)
|
||||
@@ -125,7 +112,7 @@ export function createSessionsSendTool(opts?: {
|
||||
}
|
||||
|
||||
if (requesterAgentId && requestedAgentId && requestedAgentId !== requesterAgentId) {
|
||||
if (!a2aEnabled) {
|
||||
if (!a2aPolicy.enabled) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
@@ -133,7 +120,7 @@ export function createSessionsSendTool(opts?: {
|
||||
"Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends.",
|
||||
});
|
||||
}
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(requestedAgentId)) {
|
||||
if (!a2aPolicy.isAllowed(requesterAgentId, requestedAgentId)) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
@@ -195,14 +182,26 @@ export function createSessionsSendTool(opts?: {
|
||||
error: "Either sessionKey or label is required",
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: sessionKey,
|
||||
const resolvedSession = await resolveSessionReference({
|
||||
sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
requesterInternalKey,
|
||||
restrictToSpawned,
|
||||
});
|
||||
if (!resolvedSession.ok) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: resolvedSession.status,
|
||||
error: resolvedSession.error,
|
||||
});
|
||||
}
|
||||
// Normalize sessionKey/sessionId input into a canonical session key.
|
||||
const resolvedKey = resolvedSession.key;
|
||||
const displayKey = resolvedSession.displayKey;
|
||||
const resolvedViaSessionId = resolvedSession.resolvedViaSessionId;
|
||||
|
||||
if (restrictToSpawned) {
|
||||
if (restrictToSpawned && !resolvedViaSessionId) {
|
||||
const sessions = await listSessions({
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
@@ -215,11 +214,7 @@ export function createSessionsSendTool(opts?: {
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
||||
sessionKey: resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
}),
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -231,18 +226,11 @@ export function createSessionsSendTool(opts?: {
|
||||
const announceTimeoutMs = timeoutSeconds === 0 ? 30_000 : timeoutMs;
|
||||
const idempotencyKey = crypto.randomUUID();
|
||||
let runId: string = idempotencyKey;
|
||||
const displayKey = resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
const requesterAgentId = normalizeAgentId(
|
||||
parseAgentSessionKey(requesterInternalKey)?.agentId,
|
||||
);
|
||||
const targetAgentId = normalizeAgentId(parseAgentSessionKey(resolvedKey)?.agentId);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(requesterInternalKey);
|
||||
const targetAgentId = resolveAgentIdFromSessionKey(resolvedKey);
|
||||
const isCrossAgent = requesterAgentId !== targetAgentId;
|
||||
if (isCrossAgent) {
|
||||
if (!a2aEnabled) {
|
||||
if (!a2aPolicy.enabled) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
@@ -251,7 +239,7 @@ export function createSessionsSendTool(opts?: {
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
if (!matchesAllow(requesterAgentId) || !matchesAllow(targetAgentId)) {
|
||||
if (!a2aPolicy.isAllowed(requesterAgentId, targetAgentId)) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
|
||||
@@ -38,6 +38,7 @@ export const SessionsPreviewParamsSchema = Type.Object(
|
||||
export const SessionsResolveParamsSchema = Type.Object(
|
||||
{
|
||||
key: Type.Optional(NonEmptyString),
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
label: Type.Optional(SessionLabelString),
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
spawnedBy: Type.Optional(NonEmptyString),
|
||||
|
||||
@@ -142,6 +142,12 @@ describe("gateway server sessions", () => {
|
||||
expect(resolvedByKey.ok).toBe(true);
|
||||
expect(resolvedByKey.payload?.key).toBe("agent:main:main");
|
||||
|
||||
const resolvedBySessionId = await rpcReq<{ ok: true; key: string }>(ws, "sessions.resolve", {
|
||||
sessionId: "sess-group",
|
||||
});
|
||||
expect(resolvedBySessionId.ok).toBe(true);
|
||||
expect(resolvedBySessionId.payload?.key).toBe("agent:main:discord:group:dev");
|
||||
|
||||
const list1 = await rpcReq<{
|
||||
path: string;
|
||||
defaults?: { model?: string | null; modelProvider?: string | null };
|
||||
|
||||
@@ -23,17 +23,23 @@ export function resolveSessionKeyFromResolveParams(params: {
|
||||
|
||||
const key = typeof p.key === "string" ? p.key.trim() : "";
|
||||
const hasKey = key.length > 0;
|
||||
const sessionId = typeof p.sessionId === "string" ? p.sessionId.trim() : "";
|
||||
const hasSessionId = sessionId.length > 0;
|
||||
const hasLabel = typeof p.label === "string" && p.label.trim().length > 0;
|
||||
if (hasKey && hasLabel) {
|
||||
const selectionCount = [hasKey, hasSessionId, hasLabel].filter(Boolean).length;
|
||||
if (selectionCount > 1) {
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, "Provide either key or label (not both)"),
|
||||
error: errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"Provide either key, sessionId, or label (not multiple)",
|
||||
),
|
||||
};
|
||||
}
|
||||
if (!hasKey && !hasLabel) {
|
||||
if (selectionCount === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, "Either key or label is required"),
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, "Either key, sessionId, or label is required"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +56,43 @@ export function resolveSessionKeyFromResolveParams(params: {
|
||||
return { ok: true, key: target.canonicalKey };
|
||||
}
|
||||
|
||||
if (hasSessionId) {
|
||||
const { storePath, store } = loadCombinedSessionStoreForGateway(cfg);
|
||||
const list = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath,
|
||||
store,
|
||||
opts: {
|
||||
includeGlobal: p.includeGlobal === true,
|
||||
includeUnknown: p.includeUnknown === true,
|
||||
spawnedBy: p.spawnedBy,
|
||||
agentId: p.agentId,
|
||||
search: sessionId,
|
||||
limit: 8,
|
||||
},
|
||||
});
|
||||
const matches = list.sessions.filter(
|
||||
(session) => session.sessionId === sessionId || session.key === sessionId,
|
||||
);
|
||||
if (matches.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, `No session found: ${sessionId}`),
|
||||
};
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
const keys = matches.map((session) => session.key).join(", ");
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
`Multiple sessions found for sessionId: ${sessionId} (${keys})`,
|
||||
),
|
||||
};
|
||||
}
|
||||
return { ok: true, key: String(matches[0]?.key ?? "") };
|
||||
}
|
||||
|
||||
const parsedLabel = parseSessionLabel(p.label);
|
||||
if (!parsedLabel.ok) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user