command hq: the api, mounted and documented
full REST surface for categories, tags and commanders. core permission (personal: ['/'], no grant to hold — every query is scoped to the caller's own user_id at the sql layer regardless). registered in the permission registry and PROTECTED_MOUNTS together, so totality holds by construction. GET /commanders is the grid endpoint: everything /chat/sessions already returns per session, plus categoryId/tagIds/running. `running` is derived live off collectRunningTasks (now exported from websocket.ts) folded over chat_session_events — not a stored column, on purpose, matching the design doc's stance that a status field would go stale exactly when a sidecar restart silently kills a watcher. GET /commanders/:sessionId deliberately does NOT duplicate the transcript — that stays GET /chat/sessions/:id, unchanged. a client calls both. /chat/sessions now excludes anything filed as a commander (listCommandHqSessionIds, .catch()'d so a hiccup on this feature can't take the whole chat list down) — the other half of the one predicate this whole design turns on. there is deliberately no POST /commanders/new: creation is an ordinary chat turn in the commanders directory, which is the next commit. docs/command-hq.md gains an API reference table for the mobile dev, and a status line at the top saying plainly what is real and what is not yet: needs `bun db:push` before any route returns real data, frontend not started. bunx tsgo clean. suite at baseline (same 7 pre-existing failures, all registry/totality drift and the pty socket test — none touch this router). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { pauseBackgroundTask, restartBackgroundTask } from './task-restart';
|
||||
import { createRouter } from '../../create-router';
|
||||
import * as errors from '@@/custom-errors';
|
||||
import * as sidecar from '@@/sidecar-registry';
|
||||
import { getUserSettings, getChatSessionModel, getChatSessionModels } from 'officerdb';
|
||||
import { getUserSettings, getChatSessionModel, getChatSessionModels, listCommandHqSessionIds } from 'officerdb';
|
||||
import {
|
||||
listClaudePwds,
|
||||
listClaudeSessions,
|
||||
@@ -109,7 +109,8 @@ chatRouter.get('/pwds', async (ctx) => {
|
||||
// GET /chat/sessions[?cwd=] — conversations for a working directory, merged across both harnesses
|
||||
// (Claude transcripts + OpenCode's session store), newest first.
|
||||
chatRouter.get('/sessions', async (ctx) => {
|
||||
const who = await chatIdentity(ctx.get('user'));
|
||||
const user = ctx.get('user');
|
||||
const who = await chatIdentity(user);
|
||||
const cwd = cwdOf(ctx, who.home);
|
||||
|
||||
// `?all=1` — every directory at once, for the list's Latest tab. A separate parameter rather than a
|
||||
@@ -128,7 +129,20 @@ chatRouter.get('/sessions', async (ctx) => {
|
||||
// inventing one by looping would need a directory list it does not publish. So Latest is Claude-only
|
||||
// for now, which is honest — every OpenCode session is still reachable from its own directory in Tree.
|
||||
const opencode = who.isOwner && !all ? await listOpenCodeSessions(cwd) : [];
|
||||
const sessions = [...claude, ...opencode].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
const merged = [...claude, ...opencode].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
|
||||
/*
|
||||
Command HQ's filed commanders are excluded here, never rendered as a second conversation somewhere
|
||||
else. This is the opposite side of the ONE predicate `command_hq_sessions` exists to be — Command HQ's
|
||||
own grid includes exactly what this excludes. Fetched fresh per request rather than cached: the set is
|
||||
small (one row per commander) and staleness here would mean a filed commander briefly reappearing in
|
||||
`/chat`, which is a worse failure than the extra query.
|
||||
|
||||
Never fatal — a database hiccup on a feature /chat does not otherwise depend on must not take the whole
|
||||
conversation list down with it, the same posture `getChatSessionModels` below already takes.
|
||||
*/
|
||||
const filed = await listCommandHqSessionIds(user.id).catch(() => new Set<string>());
|
||||
const sessions = merged.filter((s) => !filed.has(s.id));
|
||||
|
||||
/*
|
||||
The model each session is on, batched.
|
||||
|
||||
Reference in New Issue
Block a user