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: logs tab opens at the newest entries (bottom).
|
||||||
- Control UI: add Docs link, remove chat composer divider, and add New session button.
|
- 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
|
- 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: 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: 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).
|
- Control UI: drop explicit `ui:install` step; `ui:build` now auto-installs UI deps (docs + update flow).
|
||||||
|
|||||||
@@ -29,12 +29,24 @@ import {
|
|||||||
resolvePingPongTurns,
|
resolvePingPongTurns,
|
||||||
} from "./sessions-send-helpers.js";
|
} from "./sessions-send-helpers.js";
|
||||||
|
|
||||||
const SessionsSendToolSchema = Type.Object({
|
const SessionsSendToolSchema = Type.Union([
|
||||||
sessionKey: Type.Optional(Type.String()),
|
Type.Object(
|
||||||
label: Type.Optional(Type.String()),
|
{
|
||||||
message: Type.String(),
|
sessionKey: Type.String(),
|
||||||
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
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?: {
|
export function createSessionsSendTool(opts?: {
|
||||||
agentSessionKey?: string;
|
agentSessionKey?: string;
|
||||||
@@ -49,36 +61,8 @@ export function createSessionsSendTool(opts?: {
|
|||||||
parameters: SessionsSendToolSchema,
|
parameters: SessionsSendToolSchema,
|
||||||
execute: async (_toolCallId, args) => {
|
execute: async (_toolCallId, args) => {
|
||||||
const params = args as Record<string, unknown>;
|
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 message = readStringParam(params, "message", { required: true });
|
||||||
const cfg = loadConfig();
|
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 { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||||
const visibility =
|
const visibility =
|
||||||
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||||
@@ -90,42 +74,111 @@ export function createSessionsSendTool(opts?: {
|
|||||||
mainKey,
|
mainKey,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
const resolvedKey = resolveInternalSessionKey({
|
|
||||||
key: sessionKey,
|
|
||||||
alias,
|
|
||||||
mainKey,
|
|
||||||
});
|
|
||||||
const restrictToSpawned =
|
const restrictToSpawned =
|
||||||
opts?.sandboxed === true &&
|
opts?.sandboxed === true &&
|
||||||
visibility === "spawned" &&
|
visibility === "spawned" &&
|
||||||
requesterInternalKey &&
|
requesterInternalKey &&
|
||||||
!isSubagentSessionKey(requesterInternalKey);
|
!isSubagentSessionKey(requesterInternalKey);
|
||||||
if (restrictToSpawned) {
|
|
||||||
try {
|
const sessionKeyParam = readStringParam(params, "sessionKey");
|
||||||
const list = (await callGateway({
|
const labelParam = readStringParam(params, "label");
|
||||||
method: "sessions.list",
|
if (sessionKeyParam && labelParam) {
|
||||||
params: {
|
return jsonResult({
|
||||||
includeGlobal: false,
|
runId: crypto.randomUUID(),
|
||||||
includeUnknown: false,
|
status: "error",
|
||||||
limit: 500,
|
error: "Provide either sessionKey or label (not both).",
|
||||||
spawnedBy: requesterInternalKey,
|
});
|
||||||
},
|
}
|
||||||
})) as { sessions?: Array<Record<string, unknown>> };
|
|
||||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
const listSessions = async (listParams: Record<string, unknown>) => {
|
||||||
const ok = sessions.some((entry) => entry?.key === resolvedKey);
|
const result = (await callGateway({
|
||||||
if (!ok) {
|
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({
|
return jsonResult({
|
||||||
runId: crypto.randomUUID(),
|
runId: crypto.randomUUID(),
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
error: `Session not visible from this sandboxed agent session: label=${labelParam}`,
|
||||||
sessionKey: resolveDisplaySessionKey({
|
|
||||||
key: sessionKey,
|
|
||||||
alias,
|
|
||||||
mainKey,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} 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({
|
return jsonResult({
|
||||||
runId: crypto.randomUUID(),
|
runId: crypto.randomUUID(),
|
||||||
status: "forbidden",
|
status: "forbidden",
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export async function runReplyAgent(params: {
|
|||||||
if (steered && !shouldFollowup) {
|
if (steered && !shouldFollowup) {
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ export async function runReplyAgent(params: {
|
|||||||
enqueueFollowupRun(queueKey, followupRun, resolvedQueue);
|
enqueueFollowupRun(queueKey, followupRun, resolvedQueue);
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
@@ -674,7 +674,7 @@ export async function runReplyAgent(params: {
|
|||||||
) {
|
) {
|
||||||
sessionEntry.groupActivationNeedsSystemIntro = false;
|
sessionEntry.groupActivationNeedsSystemIntro = false;
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -880,7 +880,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1099,7 @@ export async function persistInlineDirectives(params: {
|
|||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export async function createModelSelectionState(params: {
|
|||||||
delete sessionEntry.providerOverride;
|
delete sessionEntry.providerOverride;
|
||||||
delete sessionEntry.modelOverride;
|
delete sessionEntry.modelOverride;
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ export async function createModelSelectionState(params: {
|
|||||||
if (!profile || profile.provider !== provider) {
|
if (!profile || profile.provider !== provider) {
|
||||||
delete sessionEntry.authProfileOverride;
|
delete sessionEntry.authProfileOverride;
|
||||||
sessionEntry.updatedAt = Date.now();
|
sessionEntry.updatedAt = Date.now();
|
||||||
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
sessionStore[sessionKey] = sessionEntry;
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ describe("sessions_send label lookup", () => {
|
|||||||
};
|
};
|
||||||
expect(details.status).toBe("ok");
|
expect(details.status).toBe("ok");
|
||||||
expect(details.reply).toBe("labeled response");
|
expect(details.reply).toBe("labeled response");
|
||||||
expect(details.sessionKey).toBe("test-labeled-session");
|
expect(details.sessionKey).toBe("agent:main:test-labeled-session");
|
||||||
} finally {
|
} finally {
|
||||||
if (prevPort === undefined) {
|
if (prevPort === undefined) {
|
||||||
delete process.env.CLAWDBOT_GATEWAY_PORT;
|
delete process.env.CLAWDBOT_GATEWAY_PORT;
|
||||||
|
|||||||
Reference in New Issue
Block a user