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:
Azade
2026-01-08 23:17:08 +00:00
committed by Peter Steinberger
parent b85854d0fe
commit 3133c7c84e
20 changed files with 142 additions and 27 deletions

View File

@@ -52,6 +52,8 @@ export const agentHandlers: GatewayRequestHandlers = {
extraSystemPrompt?: string;
idempotencyKey: string;
timeout?: number;
label?: string;
spawnedBy?: string;
};
const idem = request.idempotencyKey;
const cached = context.dedupe.get(`agent:${idem}`);
@@ -78,6 +80,8 @@ export const agentHandlers: GatewayRequestHandlers = {
cfgForAgent = cfg;
const now = Date.now();
const sessionId = entry?.sessionId ?? randomUUID();
const labelValue = request.label?.trim() || entry?.label;
const spawnedByValue = request.spawnedBy?.trim() || entry?.spawnedBy;
const nextEntry: SessionEntry = {
sessionId,
updatedAt: now,
@@ -91,6 +95,8 @@ export const agentHandlers: GatewayRequestHandlers = {
lastTo: entry?.lastTo,
modelOverride: entry?.modelOverride,
providerOverride: entry?.providerOverride,
label: labelValue,
spawnedBy: spawnedByValue,
};
sessionEntry = nextEntry;
const sendPolicy = resolveSendPolicy({

View File

@@ -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) {
const raw = p.thinkingLevel;
if (raw === null) {
@@ -422,6 +440,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
model: entry?.model,
contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy,
label: entry?.label,
lastProvider: entry?.lastProvider,
lastTo: entry?.lastTo,
skillsSnapshot: entry?.skillsSnapshot,