a spawned session says who started it

The other half of making a spawned agent visible. The commander's pane now lists
its children; opening one gave no way back — no link to the conversation that owns
it, and nothing to tell it apart from a chat somebody had started themselves and
forgotten.

`parentCommanderOf` is the row `ingestSpawnedChildren` already writes, read from
the other end. So this needed no table and no write path: a commander's children
were recorded because deleting one has to take them with it, and that record
answers "who is my parent" for free. Nothing had asked.

`GET /chat/sessions/:id` carries `spawnedBy`, and the chat panel renders one line
above the transcript linking back to the commander. Null for almost every session,
which is the normal case rather than a miss — a chat somebody opened has no parent
— so the bar renders nothing at all rather than saying so. Never fatal: a session
must open when this lookup will not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 05:09:28 +00:00
co-authored by Claude Opus 5
parent 44f5246e38
commit f6b9ddc877
5 changed files with 85 additions and 0 deletions
@@ -15,6 +15,7 @@ export {
disarmCommandHqWatcher,
recordCommandHqTurn,
ingestSpawnedChildren,
parentCommanderOf,
recordSpawnedOutcome,
getCommandHqCostSummary,
listCommandHqTurns,
@@ -475,3 +475,44 @@ export async function recordSpawnedOutcome(
and(eq(commandHqSpawned.sessionRowId, sessionRowId), eq(commandHqSpawned.spawnedSessionId, spawnedSessionId)),
);
}
export type SpawnParent = {
/** The commander's transcript id — what `/command-hq/<id>` opens. */
sessionId: string;
/** What the owner called it, or null for a commander filed before the column existed. */
name: string | null;
firstSeenAt: string;
finishedAt: string | null;
};
/**
* Which commander spawned this session, if any.
*
* The same row `ingestSpawnedChildren` writes, read from the other end — so this needed no new table and no
* new write path. A commander's children were already recorded because deleting one has to take them with
* it; that record answers "who is my parent" for free, and nothing was asking.
*
* Null for almost every session, and that is the normal case rather than a miss: a chat somebody opened
* themselves has no parent, and a session spawned by anything other than a commander has no row here. The
* caller must treat absence as "not spawned", never as "lookup failed".
*/
export async function parentCommanderOf(spawnedSessionId: string): Promise<SpawnParent | null> {
const [row] = await db
.select({
sessionId: commandHqSessions.sessionId,
name: commandHqSessions.name,
firstSeenAt: commandHqSpawned.firstSeenAt,
finishedAt: commandHqSpawned.finishedAt,
})
.from(commandHqSpawned)
.innerJoin(commandHqSessions, eq(commandHqSessions.id, commandHqSpawned.sessionRowId))
.where(eq(commandHqSpawned.spawnedSessionId, spawnedSessionId))
.limit(1);
if (!row) return null;
return {
sessionId: row.sessionId,
name: row.name,
firstSeenAt: row.firstSeenAt.toISOString(),
finishedAt: row.finishedAt ? row.finishedAt.toISOString() : null,
};
}