feat: session source in session_meta and an Automated sidebar folder (#40)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-23 00:58:43 +08:00
committed by GitHub
parent e0bf1706b9
commit c380113e26
30 changed files with 1023 additions and 74 deletions
+16 -3
View File
@@ -30,6 +30,7 @@ import { sseEndpoint } from "../sse.js";
import {
badRequest,
optionalEnum,
optionalPagingQuery,
optionalString,
paginationQuery,
pathParam,
@@ -96,7 +97,14 @@ export function agentSessionsRoutes(deps: AppDeps): Hono<AppEnv> {
const agentId = requireValidId(c, "agentId");
deps.projectService.requireProjectAccess(c.var.user.userId, projectId);
await deps.agentConfigService.requireExists(projectId, agentId);
const sessions = await deps.sessionService.listSessions(projectId, agentId);
// Optional paging (absent = full list, the pre-paging contract): the sidebar requests
// limit+1 and shows limit, detecting "has more" without a response-envelope change.
const paging = optionalPagingQuery(c);
const sessions = await deps.sessionService.listSessions(
projectId,
agentId,
...(paging ? [paging] : []),
);
return c.json({ sessions } satisfies SessionsResponse);
});
@@ -167,7 +175,9 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
app.get("/:sessionId", async (c) => {
const row = resolveSession(c);
const hasTrace = await deps.sessionService.hasTrace(row);
return c.json({ session: deps.sessionService.toInfo(row, hasTrace) } satisfies SessionResponse);
return c.json({
session: await deps.sessionService.toInfo(row, hasTrace),
} satisfies SessionResponse);
});
app.patch("/:sessionId", async (c) => {
@@ -216,7 +226,7 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
}
const hasTrace = await deps.sessionService.hasTrace(updated);
return c.json({
session: deps.sessionService.toInfo(updated, hasTrace),
session: await deps.sessionService.toInfo(updated, hasTrace),
} satisfies SessionResponse);
});
@@ -244,6 +254,9 @@ export function sessionsRoutes(deps: AppDeps): Hono<AppEnv> {
{ recursive: true, force: true },
);
deps.sessionsRepo.deleteById(row.sessionId);
// Drop the derived-origin entry along with the Session (bulk Agent/Project deletion
// may leave stale entries; session ids are never reused, so they are never matched).
deps.sessionSources.delete(row.sessionId);
} finally {
deps.manager.endSessionDeletion(row.sessionId);
}
+22
View File
@@ -64,6 +64,28 @@ export function paginationQuery(c: Context): { offset: number; limit: number } {
return { offset, limit };
}
/**
* Parse OPTIONAL list paging query params: both absent = null (caller returns the full
* list — the pre-paging behavior stays intact for existing callers). When paging, `limit`
* is required (1-1000) and `offset` optional (>= 0, default 0) — an offset alone would
* silently return the full list shifted, which no caller ever means.
*/
export function optionalPagingQuery(c: Context): { offset: number; limit: number } | null {
const rawLimit = c.req.query("limit");
const rawOffset = c.req.query("offset");
if (rawLimit === undefined && rawOffset === undefined) return null;
if (rawLimit === undefined) throw badRequest("offset requires limit.");
const limit = Number.parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
throw badRequest("limit must be an integer between 1 and 1000.");
}
const offset = Number.parseInt(rawOffset ?? "0", 10);
if (!Number.isInteger(offset) || offset < 0) {
throw badRequest("offset must be a non-negative integer.");
}
return { offset, limit };
}
/** Read the JSON request body (parse failure / non-object -> 400). */
export async function readJson(c: Context): Promise<Record<string, unknown>> {
let body: unknown;