fix: sessions label lookup and persistence (#570) (thanks @azade-c)
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
- Control UI: logs tab opens at the newest entries (bottom).
|
||||
- Control UI: add Docs link, remove chat composer divider, and add New session button.
|
||||
- Control UI: link sessions list to chat view. (#471) — thanks @HazAT
|
||||
- Sessions: support session `label` in store/list/UI and allow `sessions_send` lookup by label. (#570) — thanks @azade-c
|
||||
- Control UI: show/patch per-session reasoning level and render extracted reasoning in chat.
|
||||
- Control UI: queue outgoing chat messages, add Enter-to-send, and show queued items. (#527) — thanks @YuriNachos
|
||||
- Control UI: drop explicit `ui:install` step; `ui:build` now auto-installs UI deps (docs + update flow).
|
||||
|
||||
@@ -84,7 +84,7 @@ function ensureListener() {
|
||||
? (evt.data.endedAt as number)
|
||||
: Date.now();
|
||||
entry.endedAt = endedAt;
|
||||
|
||||
|
||||
if (!beginSubagentAnnounce(evt.runId)) {
|
||||
if (entry.cleanup === "delete") {
|
||||
subagentRuns.delete(evt.runId);
|
||||
|
||||
@@ -29,12 +29,24 @@ import {
|
||||
resolvePingPongTurns,
|
||||
} from "./sessions-send-helpers.js";
|
||||
|
||||
const SessionsSendToolSchema = Type.Object({
|
||||
sessionKey: Type.Optional(Type.String()),
|
||||
label: Type.Optional(Type.String()),
|
||||
message: Type.String(),
|
||||
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
});
|
||||
const SessionsSendToolSchema = Type.Union([
|
||||
Type.Object(
|
||||
{
|
||||
sessionKey: Type.String(),
|
||||
message: Type.String(),
|
||||
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
Type.Object(
|
||||
{
|
||||
label: Type.String(),
|
||||
message: Type.String(),
|
||||
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
),
|
||||
]);
|
||||
|
||||
export function createSessionsSendTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
@@ -49,36 +61,8 @@ export function createSessionsSendTool(opts?: {
|
||||
parameters: SessionsSendToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
let sessionKey = readStringParam(params, "sessionKey");
|
||||
const labelParam = readStringParam(params, "label");
|
||||
const message = readStringParam(params, "message", { required: true });
|
||||
const cfg = loadConfig();
|
||||
|
||||
// Lookup by label if sessionKey not provided
|
||||
if (!sessionKey && labelParam) {
|
||||
const listResult = (await callGateway({
|
||||
method: "sessions.list",
|
||||
params: { activeMinutes: 1440 }, // Last 24h
|
||||
timeoutMs: 10_000,
|
||||
})) as { sessions?: Array<{ key: string; label?: string }> };
|
||||
const match = listResult.sessions?.find(
|
||||
(s) => s.label === labelParam,
|
||||
);
|
||||
if (!match) {
|
||||
return jsonResult({
|
||||
status: "error",
|
||||
error: `No session found with label: ${labelParam}`,
|
||||
});
|
||||
}
|
||||
sessionKey = match.key;
|
||||
}
|
||||
|
||||
if (!sessionKey) {
|
||||
return jsonResult({
|
||||
status: "error",
|
||||
error: "Either sessionKey or label is required",
|
||||
});
|
||||
}
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility =
|
||||
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
@@ -90,42 +74,111 @@ export function createSessionsSendTool(opts?: {
|
||||
mainKey,
|
||||
})
|
||||
: undefined;
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
requesterInternalKey &&
|
||||
!isSubagentSessionKey(requesterInternalKey);
|
||||
if (restrictToSpawned) {
|
||||
try {
|
||||
const list = (await callGateway({
|
||||
method: "sessions.list",
|
||||
params: {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit: 500,
|
||||
spawnedBy: requesterInternalKey,
|
||||
},
|
||||
})) as { sessions?: Array<Record<string, unknown>> };
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
const ok = sessions.some((entry) => entry?.key === resolvedKey);
|
||||
if (!ok) {
|
||||
|
||||
const sessionKeyParam = readStringParam(params, "sessionKey");
|
||||
const labelParam = readStringParam(params, "label");
|
||||
if (sessionKeyParam && labelParam) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "error",
|
||||
error: "Provide either sessionKey or label (not both).",
|
||||
});
|
||||
}
|
||||
|
||||
const listSessions = async (listParams: Record<string, unknown>) => {
|
||||
const result = (await callGateway({
|
||||
method: "sessions.list",
|
||||
params: listParams,
|
||||
timeoutMs: 10_000,
|
||||
})) as { sessions?: Array<Record<string, unknown>> };
|
||||
return Array.isArray(result?.sessions) ? result.sessions : [];
|
||||
};
|
||||
|
||||
const activeMinutes = 24 * 60;
|
||||
const visibleSessions = restrictToSpawned
|
||||
? await listSessions({
|
||||
activeMinutes,
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit: 500,
|
||||
spawnedBy: requesterInternalKey,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
let sessionKey = sessionKeyParam;
|
||||
if (!sessionKey && labelParam) {
|
||||
const sessions =
|
||||
visibleSessions ??
|
||||
(await listSessions({
|
||||
activeMinutes,
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit: 500,
|
||||
}));
|
||||
const matches = sessions.filter((entry) => {
|
||||
const label =
|
||||
typeof entry?.label === "string" ? entry.label : undefined;
|
||||
return label === labelParam;
|
||||
});
|
||||
if (matches.length === 0) {
|
||||
if (restrictToSpawned) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
||||
sessionKey: resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
}),
|
||||
error: `Session not visible from this sandboxed agent session: label=${labelParam}`,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "error",
|
||||
error: `No session found with label: ${labelParam}`,
|
||||
});
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
const keys = matches
|
||||
.map((entry) => (typeof entry?.key === "string" ? entry.key : ""))
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "error",
|
||||
error: `Multiple sessions found with label: ${labelParam}${keys ? ` (${keys})` : ""}`,
|
||||
});
|
||||
}
|
||||
const key = matches[0]?.key;
|
||||
if (typeof key !== "string" || !key.trim()) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "error",
|
||||
error: `Invalid session entry for label: ${labelParam}`,
|
||||
});
|
||||
}
|
||||
sessionKey = key;
|
||||
}
|
||||
|
||||
if (!sessionKey) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "error",
|
||||
error: "Either sessionKey or label is required",
|
||||
});
|
||||
}
|
||||
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
|
||||
if (restrictToSpawned) {
|
||||
const sessions = visibleSessions ?? [];
|
||||
const ok = sessions.some((entry) => entry?.key === resolvedKey);
|
||||
if (!ok) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
|
||||
@@ -271,7 +271,7 @@ export async function runReplyAgent(params: {
|
||||
if (steered && !shouldFollowup) {
|
||||
if (sessionEntry && sessionStore && sessionKey) {
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
@@ -285,7 +285,7 @@ export async function runReplyAgent(params: {
|
||||
enqueueFollowupRun(queueKey, followupRun, resolvedQueue);
|
||||
if (sessionEntry && sessionStore && sessionKey) {
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
@@ -674,7 +674,7 @@ export async function runReplyAgent(params: {
|
||||
) {
|
||||
sessionEntry.groupActivationNeedsSystemIntro = false;
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
|
||||
@@ -880,7 +880,7 @@ export async function handleDirectiveOnly(params: {
|
||||
}
|
||||
}
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
@@ -1099,7 +1099,7 @@ export async function persistInlineDirectives(params: {
|
||||
}
|
||||
if (updated) {
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export async function createModelSelectionState(params: {
|
||||
delete sessionEntry.providerOverride;
|
||||
delete sessionEntry.modelOverride;
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export async function createModelSelectionState(params: {
|
||||
if (!profile || profile.provider !== provider) {
|
||||
delete sessionEntry.authProfileOverride;
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ describe("sessions_send label lookup", () => {
|
||||
};
|
||||
expect(details.status).toBe("ok");
|
||||
expect(details.reply).toBe("labeled response");
|
||||
expect(details.sessionKey).toBe("test-labeled-session");
|
||||
expect(details.sessionKey).toBe("agent:main:test-labeled-session");
|
||||
} finally {
|
||||
if (prevPort === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_PORT;
|
||||
|
||||
Reference in New Issue
Block a user