feat(sessions): expose label in sessions.list and support label lookup in sessions_send
- Add `label` field to session entries and expose it in `sessions.list`
- Display label column in the web UI sessions table
- Support `label` parameter in `sessions_send` for lookup by label instead of sessionKey
- `sessions.patch`: Accept and store `label` field
- `sessions.list`: Return `label` in session entries
- `sessions_spawn`: Pass label through to registry and announce flow
- `sessions_send`: Accept optional `label` param, lookup session by label if sessionKey not provided
- `agent` method: Accept `label` and `spawnedBy` params (stored in session entry)
- Add `label` column to sessions table in web UI
- Changed session store writes to merge with existing entry (`{ ...existing, ...new }`)
to preserve fields like `label` that might be set separately
We attempted to implement label persistence "properly" by passing the label
through the `agent` call and storing it during session initialization. However,
the auto-reply flow has multiple write points that overwrite the session entry,
and making all of them merge-aware proved unreliable.
The working solution patches the label in the `finally` block of
`runSubagentAnnounceFlow`, after all other session writes complete.
This is a workaround but robust - the patch happens at the very end,
just before potential cleanup.
A future refactor could make session writes consistently merge-based,
which would allow the cleaner approach of setting label at spawn time.
```typescript
// Spawn with label
sessions_spawn({ task: "...", label: "my-worker" })
// Later, find by label
sessions_send({ label: "my-worker", message: "continue..." })
// Or use sessions_list to see labels
sessions_list() // includes label field in response
```
This commit is contained in:
@@ -196,6 +196,7 @@ export async function runSubagentAnnounceFlow(params: {
|
|||||||
waitForCompletion?: boolean;
|
waitForCompletion?: boolean;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
endedAt?: number;
|
endedAt?: number;
|
||||||
|
label?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
let reply = params.roundOneReply;
|
let reply = params.roundOneReply;
|
||||||
@@ -273,6 +274,18 @@ export async function runSubagentAnnounceFlow(params: {
|
|||||||
} catch {
|
} catch {
|
||||||
// Best-effort follow-ups; ignore failures to avoid breaking the caller response.
|
// Best-effort follow-ups; ignore failures to avoid breaking the caller response.
|
||||||
} finally {
|
} finally {
|
||||||
|
// Patch label after all writes complete
|
||||||
|
if (params.label) {
|
||||||
|
try {
|
||||||
|
await callGateway({
|
||||||
|
method: "sessions.patch",
|
||||||
|
params: { key: params.childSessionKey, label: params.label },
|
||||||
|
timeoutMs: 10_000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
if (params.cleanup === "delete") {
|
if (params.cleanup === "delete") {
|
||||||
try {
|
try {
|
||||||
await callGateway({
|
await callGateway({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type SubagentRunRecord = {
|
|||||||
requesterDisplayKey: string;
|
requesterDisplayKey: string;
|
||||||
task: string;
|
task: string;
|
||||||
cleanup: "delete" | "keep";
|
cleanup: "delete" | "keep";
|
||||||
|
label?: string;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
endedAt?: number;
|
endedAt?: number;
|
||||||
@@ -83,6 +84,7 @@ function ensureListener() {
|
|||||||
? (evt.data.endedAt as number)
|
? (evt.data.endedAt as number)
|
||||||
: Date.now();
|
: Date.now();
|
||||||
entry.endedAt = endedAt;
|
entry.endedAt = endedAt;
|
||||||
|
|
||||||
if (!beginSubagentAnnounce(evt.runId)) {
|
if (!beginSubagentAnnounce(evt.runId)) {
|
||||||
if (entry.cleanup === "delete") {
|
if (entry.cleanup === "delete") {
|
||||||
subagentRuns.delete(evt.runId);
|
subagentRuns.delete(evt.runId);
|
||||||
@@ -101,6 +103,7 @@ function ensureListener() {
|
|||||||
waitForCompletion: false,
|
waitForCompletion: false,
|
||||||
startedAt: entry.startedAt,
|
startedAt: entry.startedAt,
|
||||||
endedAt: entry.endedAt,
|
endedAt: entry.endedAt,
|
||||||
|
label: entry.label,
|
||||||
});
|
});
|
||||||
if (entry.cleanup === "delete") {
|
if (entry.cleanup === "delete") {
|
||||||
subagentRuns.delete(evt.runId);
|
subagentRuns.delete(evt.runId);
|
||||||
@@ -124,6 +127,7 @@ export function registerSubagentRun(params: {
|
|||||||
requesterDisplayKey: string;
|
requesterDisplayKey: string;
|
||||||
task: string;
|
task: string;
|
||||||
cleanup: "delete" | "keep";
|
cleanup: "delete" | "keep";
|
||||||
|
label?: string;
|
||||||
}) {
|
}) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const archiveAfterMs = resolveArchiveAfterMs();
|
const archiveAfterMs = resolveArchiveAfterMs();
|
||||||
@@ -136,6 +140,7 @@ export function registerSubagentRun(params: {
|
|||||||
requesterDisplayKey: params.requesterDisplayKey,
|
requesterDisplayKey: params.requesterDisplayKey,
|
||||||
task: params.task,
|
task: params.task,
|
||||||
cleanup: params.cleanup,
|
cleanup: params.cleanup,
|
||||||
|
label: params.label,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
startedAt: now,
|
startedAt: now,
|
||||||
archiveAtMs,
|
archiveAtMs,
|
||||||
@@ -175,6 +180,7 @@ async function probeImmediateCompletion(runId: string) {
|
|||||||
waitForCompletion: false,
|
waitForCompletion: false,
|
||||||
startedAt: entry.startedAt,
|
startedAt: entry.startedAt,
|
||||||
endedAt: entry.endedAt,
|
endedAt: entry.endedAt,
|
||||||
|
label: entry.label,
|
||||||
});
|
});
|
||||||
if (entry.cleanup === "delete") {
|
if (entry.cleanup === "delete") {
|
||||||
subagentRuns.delete(runId);
|
subagentRuns.delete(runId);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type SessionListRow = {
|
|||||||
key: string;
|
key: string;
|
||||||
kind: SessionKind;
|
kind: SessionKind;
|
||||||
provider: string;
|
provider: string;
|
||||||
|
label?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
@@ -205,6 +206,7 @@ export function createSessionsListTool(opts?: {
|
|||||||
key: displayKey,
|
key: displayKey,
|
||||||
kind,
|
kind,
|
||||||
provider: derivedProvider,
|
provider: derivedProvider,
|
||||||
|
label: typeof entry.label === "string" ? entry.label : undefined,
|
||||||
displayName:
|
displayName:
|
||||||
typeof entry.displayName === "string"
|
typeof entry.displayName === "string"
|
||||||
? entry.displayName
|
? entry.displayName
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
} from "./sessions-send-helpers.js";
|
} from "./sessions-send-helpers.js";
|
||||||
|
|
||||||
const SessionsSendToolSchema = Type.Object({
|
const SessionsSendToolSchema = Type.Object({
|
||||||
sessionKey: Type.String(),
|
sessionKey: Type.Optional(Type.String()),
|
||||||
|
label: Type.Optional(Type.String()),
|
||||||
message: Type.String(),
|
message: Type.String(),
|
||||||
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||||
});
|
});
|
||||||
@@ -43,15 +44,41 @@ export function createSessionsSendTool(opts?: {
|
|||||||
return {
|
return {
|
||||||
label: "Session Send",
|
label: "Session Send",
|
||||||
name: "sessions_send",
|
name: "sessions_send",
|
||||||
description: "Send a message into another session.",
|
description:
|
||||||
|
"Send a message into another session. Use sessionKey or label to identify the target.",
|
||||||
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>;
|
||||||
const sessionKey = readStringParam(params, "sessionKey", {
|
let sessionKey = readStringParam(params, "sessionKey");
|
||||||
required: true,
|
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";
|
||||||
|
|||||||
@@ -126,17 +126,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||||
if (opts?.sandboxed === true) {
|
const shouldPatchSpawnedBy = opts?.sandboxed === true;
|
||||||
try {
|
|
||||||
await callGateway({
|
|
||||||
method: "sessions.patch",
|
|
||||||
params: { key: childSessionKey, spawnedBy: requesterInternalKey },
|
|
||||||
timeoutMs: 10_000,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// best-effort; scoping relies on this metadata but spawning still works without it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (model) {
|
if (model) {
|
||||||
try {
|
try {
|
||||||
await callGateway({
|
await callGateway({
|
||||||
@@ -185,6 +175,8 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
lane: "subagent",
|
lane: "subagent",
|
||||||
extraSystemPrompt: childSystemPrompt,
|
extraSystemPrompt: childSystemPrompt,
|
||||||
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
||||||
|
label: label || undefined,
|
||||||
|
spawnedBy: shouldPatchSpawnedBy ? requesterInternalKey : undefined,
|
||||||
},
|
},
|
||||||
timeoutMs: 10_000,
|
timeoutMs: 10_000,
|
||||||
})) as { runId?: string };
|
})) as { runId?: string };
|
||||||
@@ -214,6 +206,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
requesterDisplayKey,
|
requesterDisplayKey,
|
||||||
task,
|
task,
|
||||||
cleanup,
|
cleanup,
|
||||||
|
label: label || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
|
|||||||
@@ -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] = sessionEntry;
|
sessionStore[sessionKey] = { ...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] = sessionEntry;
|
sessionStore[sessionKey] = { ...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] = sessionEntry;
|
sessionStore[sessionKey] = { ...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] = sessionEntry;
|
sessionStore[sessionKey] = { ...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] = sessionEntry;
|
sessionStore[sessionKey] = { ...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] = sessionEntry;
|
sessionStore[sessionKey] = { ...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] = sessionEntry;
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
systemSent: true,
|
systemSent: true,
|
||||||
skillsSnapshot: skillSnapshot,
|
skillsSnapshot: skillSnapshot,
|
||||||
};
|
};
|
||||||
sessionStore[sessionKey] = nextEntry;
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ export async function ensureSkillSnapshot(params: {
|
|||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
skillsSnapshot,
|
skillsSnapshot,
|
||||||
};
|
};
|
||||||
sessionStore[sessionKey] = nextEntry;
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
|
||||||
if (storePath) {
|
if (storePath) {
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export async function initSessionState(params: {
|
|||||||
ctx.MessageThreadId,
|
ctx.MessageThreadId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
sessionStore[sessionKey] = sessionEntry;
|
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
|
||||||
await saveSessionStore(storePath, sessionStore);
|
await saveSessionStore(storePath, sessionStore);
|
||||||
|
|
||||||
const sessionCtx: TemplateContext = {
|
const sessionCtx: TemplateContext = {
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export type SessionEntry = {
|
|||||||
contextTokens?: number;
|
contextTokens?: number;
|
||||||
compactionCount?: number;
|
compactionCount?: number;
|
||||||
claudeCliSessionId?: string;
|
claudeCliSessionId?: string;
|
||||||
|
label?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ export const AgentParamsSchema = Type.Object(
|
|||||||
lane: Type.Optional(Type.String()),
|
lane: Type.Optional(Type.String()),
|
||||||
extraSystemPrompt: Type.Optional(Type.String()),
|
extraSystemPrompt: Type.Optional(Type.String()),
|
||||||
idempotencyKey: NonEmptyString,
|
idempotencyKey: NonEmptyString,
|
||||||
|
label: Type.Optional(Type.String()),
|
||||||
|
spawnedBy: Type.Optional(Type.String()),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
@@ -322,6 +324,7 @@ export const SessionsListParamsSchema = Type.Object(
|
|||||||
export const SessionsPatchParamsSchema = Type.Object(
|
export const SessionsPatchParamsSchema = Type.Object(
|
||||||
{
|
{
|
||||||
key: NonEmptyString,
|
key: NonEmptyString,
|
||||||
|
label: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
|||||||
@@ -397,6 +397,25 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("label" in p) {
|
||||||
|
const raw = p.label;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.label;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const trimmed = String(raw).trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
code: ErrorCodes.INVALID_REQUEST,
|
||||||
|
message: "invalid label: empty",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
next.label = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("thinkingLevel" in p) {
|
if ("thinkingLevel" in p) {
|
||||||
const raw = p.thinkingLevel;
|
const raw = p.thinkingLevel;
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
@@ -628,6 +647,7 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
|||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
sendPolicy: entry?.sendPolicy,
|
sendPolicy: entry?.sendPolicy,
|
||||||
|
label: entry?.label,
|
||||||
displayName: entry?.displayName,
|
displayName: entry?.displayName,
|
||||||
chatType: entry?.chatType,
|
chatType: entry?.chatType,
|
||||||
provider: entry?.provider,
|
provider: entry?.provider,
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
extraSystemPrompt?: string;
|
extraSystemPrompt?: string;
|
||||||
idempotencyKey: string;
|
idempotencyKey: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
label?: string;
|
||||||
|
spawnedBy?: string;
|
||||||
};
|
};
|
||||||
const idem = request.idempotencyKey;
|
const idem = request.idempotencyKey;
|
||||||
const cached = context.dedupe.get(`agent:${idem}`);
|
const cached = context.dedupe.get(`agent:${idem}`);
|
||||||
@@ -78,6 +80,8 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
cfgForAgent = cfg;
|
cfgForAgent = cfg;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sessionId = entry?.sessionId ?? randomUUID();
|
const sessionId = entry?.sessionId ?? randomUUID();
|
||||||
|
const labelValue = request.label?.trim() || entry?.label;
|
||||||
|
const spawnedByValue = request.spawnedBy?.trim() || entry?.spawnedBy;
|
||||||
const nextEntry: SessionEntry = {
|
const nextEntry: SessionEntry = {
|
||||||
sessionId,
|
sessionId,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -91,6 +95,8 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
lastTo: entry?.lastTo,
|
lastTo: entry?.lastTo,
|
||||||
modelOverride: entry?.modelOverride,
|
modelOverride: entry?.modelOverride,
|
||||||
providerOverride: entry?.providerOverride,
|
providerOverride: entry?.providerOverride,
|
||||||
|
label: labelValue,
|
||||||
|
spawnedBy: spawnedByValue,
|
||||||
};
|
};
|
||||||
sessionEntry = nextEntry;
|
sessionEntry = nextEntry;
|
||||||
const sendPolicy = resolveSendPolicy({
|
const sendPolicy = resolveSendPolicy({
|
||||||
|
|||||||
@@ -169,6 +169,24 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("label" in p) {
|
||||||
|
const raw = p.label;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.label;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const trimmed = String(raw).trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, "invalid label: empty"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next.label = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("thinkingLevel" in p) {
|
if ("thinkingLevel" in p) {
|
||||||
const raw = p.thinkingLevel;
|
const raw = p.thinkingLevel;
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
@@ -422,6 +440,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
sendPolicy: entry?.sendPolicy,
|
sendPolicy: entry?.sendPolicy,
|
||||||
|
label: entry?.label,
|
||||||
lastProvider: entry?.lastProvider,
|
lastProvider: entry?.lastProvider,
|
||||||
lastTo: entry?.lastTo,
|
lastTo: entry?.lastTo,
|
||||||
skillsSnapshot: entry?.skillsSnapshot,
|
skillsSnapshot: entry?.skillsSnapshot,
|
||||||
|
|||||||
@@ -148,12 +148,23 @@ describe("gateway server sessions", () => {
|
|||||||
expect(sendPolicyPatched.ok).toBe(true);
|
expect(sendPolicyPatched.ok).toBe(true);
|
||||||
expect(sendPolicyPatched.payload?.entry.sendPolicy).toBe("deny");
|
expect(sendPolicyPatched.payload?.entry.sendPolicy).toBe("deny");
|
||||||
|
|
||||||
|
const labelPatched = await rpcReq<{
|
||||||
|
ok: true;
|
||||||
|
entry: { label?: string };
|
||||||
|
}>(ws, "sessions.patch", {
|
||||||
|
key: "agent:main:subagent:one",
|
||||||
|
label: "Briefing",
|
||||||
|
});
|
||||||
|
expect(labelPatched.ok).toBe(true);
|
||||||
|
expect(labelPatched.payload?.entry.label).toBe("Briefing");
|
||||||
|
|
||||||
const list2 = await rpcReq<{
|
const list2 = await rpcReq<{
|
||||||
sessions: Array<{
|
sessions: Array<{
|
||||||
key: string;
|
key: string;
|
||||||
thinkingLevel?: string;
|
thinkingLevel?: string;
|
||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
sendPolicy?: string;
|
sendPolicy?: string;
|
||||||
|
label?: string;
|
||||||
}>;
|
}>;
|
||||||
}>(ws, "sessions.list", {});
|
}>(ws, "sessions.list", {});
|
||||||
expect(list2.ok).toBe(true);
|
expect(list2.ok).toBe(true);
|
||||||
@@ -163,6 +174,10 @@ describe("gateway server sessions", () => {
|
|||||||
expect(main2?.thinkingLevel).toBe("medium");
|
expect(main2?.thinkingLevel).toBe("medium");
|
||||||
expect(main2?.verboseLevel).toBeUndefined();
|
expect(main2?.verboseLevel).toBeUndefined();
|
||||||
expect(main2?.sendPolicy).toBe("deny");
|
expect(main2?.sendPolicy).toBe("deny");
|
||||||
|
const subagent = list2.payload?.sessions.find(
|
||||||
|
(s) => s.key === "agent:main:subagent:one",
|
||||||
|
);
|
||||||
|
expect(subagent?.label).toBe("Briefing");
|
||||||
|
|
||||||
const spawnedOnly = await rpcReq<{
|
const spawnedOnly = await rpcReq<{
|
||||||
sessions: Array<{ key: string }>;
|
sessions: Array<{ key: string }>;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export type GatewaySessionsDefaults = {
|
|||||||
export type GatewaySessionRow = {
|
export type GatewaySessionRow = {
|
||||||
key: string;
|
key: string;
|
||||||
kind: "direct" | "group" | "global" | "unknown";
|
kind: "direct" | "group" | "global" | "unknown";
|
||||||
|
label?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
@@ -485,6 +486,7 @@ export function listSessionsFromStore(params: {
|
|||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
kind: classifySessionKey(key, entry),
|
kind: classifySessionKey(key, entry),
|
||||||
|
label: entry?.label,
|
||||||
displayName,
|
displayName,
|
||||||
provider,
|
provider,
|
||||||
subject,
|
subject,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type GatewaySessionList = {
|
|||||||
totalTokens?: number | null;
|
totalTokens?: number | null;
|
||||||
responseUsage?: "on" | "off";
|
responseUsage?: "on" | "off";
|
||||||
modelProvider?: string;
|
modelProvider?: string;
|
||||||
|
label?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
room?: string;
|
room?: string;
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ export type GatewaySessionsDefaults = {
|
|||||||
export type GatewaySessionRow = {
|
export type GatewaySessionRow = {
|
||||||
key: string;
|
key: string;
|
||||||
kind: "direct" | "group" | "global" | "unknown";
|
kind: "direct" | "group" | "global" | "unknown";
|
||||||
|
label?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
surface?: string;
|
surface?: string;
|
||||||
subject?: string;
|
subject?: string;
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export function renderSessions(props: SessionsProps) {
|
|||||||
<div class="table" style="margin-top: 16px;">
|
<div class="table" style="margin-top: 16px;">
|
||||||
<div class="table-head">
|
<div class="table-head">
|
||||||
<div>Key</div>
|
<div>Key</div>
|
||||||
|
<div>Label</div>
|
||||||
<div>Kind</div>
|
<div>Kind</div>
|
||||||
<div>Updated</div>
|
<div>Updated</div>
|
||||||
<div>Tokens</div>
|
<div>Tokens</div>
|
||||||
@@ -132,7 +133,11 @@ export function renderSessions(props: SessionsProps) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRow(row: GatewaySessionRow, basePath: string, onPatch: SessionsProps["onPatch"]) {
|
function renderRow(
|
||||||
|
row: GatewaySessionRow,
|
||||||
|
basePath: string,
|
||||||
|
onPatch: SessionsProps["onPatch"],
|
||||||
|
) {
|
||||||
const updated = row.updatedAt ? formatAgo(row.updatedAt) : "n/a";
|
const updated = row.updatedAt ? formatAgo(row.updatedAt) : "n/a";
|
||||||
const thinking = row.thinkingLevel ?? "";
|
const thinking = row.thinkingLevel ?? "";
|
||||||
const verbose = row.verboseLevel ?? "";
|
const verbose = row.verboseLevel ?? "";
|
||||||
@@ -148,6 +153,7 @@ function renderRow(row: GatewaySessionRow, basePath: string, onPatch: SessionsPr
|
|||||||
<div class="mono">${canLink
|
<div class="mono">${canLink
|
||||||
? html`<a href=${chatUrl} class="session-link">${displayName}</a>`
|
? html`<a href=${chatUrl} class="session-link">${displayName}</a>`
|
||||||
: displayName}</div>
|
: displayName}</div>
|
||||||
|
<div>${row.label ?? ""}</div>
|
||||||
<div>${row.kind}</div>
|
<div>${row.kind}</div>
|
||||||
<div>${updated}</div>
|
<div>${updated}</div>
|
||||||
<div>${formatSessionTokens(row)}</div>
|
<div>${formatSessionTokens(row)}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user