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

@@ -271,7 +271,7 @@ export async function runReplyAgent(params: {
if (steered && !shouldFollowup) {
if (sessionEntry && sessionStore && sessionKey) {
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
sessionStore[sessionKey] = { ...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] = sessionEntry;
sessionStore[sessionKey] = { ...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] = sessionEntry;
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}

View File

@@ -880,7 +880,7 @@ export async function handleDirectiveOnly(params: {
}
}
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
sessionStore[sessionKey] = { ...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] = sessionEntry;
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}

View File

@@ -95,7 +95,7 @@ export async function createModelSelectionState(params: {
delete sessionEntry.providerOverride;
delete sessionEntry.modelOverride;
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
sessionStore[sessionKey] = { ...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] = sessionEntry;
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}

View File

@@ -91,7 +91,7 @@ export async function ensureSkillSnapshot(params: {
systemSent: true,
skillsSnapshot: skillSnapshot,
};
sessionStore[sessionKey] = nextEntry;
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
@@ -123,7 +123,7 @@ export async function ensureSkillSnapshot(params: {
updatedAt: Date.now(),
skillsSnapshot,
};
sessionStore[sessionKey] = nextEntry;
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...nextEntry };
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}

View File

@@ -264,7 +264,7 @@ export async function initSessionState(params: {
ctx.MessageThreadId,
);
}
sessionStore[sessionKey] = sessionEntry;
sessionStore[sessionKey] = { ...sessionStore[sessionKey], ...sessionEntry };
await saveSessionStore(storePath, sessionStore);
const sessionCtx: TemplateContext = {