refactor(server): extract WorkspaceDirectory from session.ts (#667)

Move workspace descriptor map building, fetch_workspaces filtering /
sorting / cursor paging, and archiving overlay state out of the Session
god class into a dedicated WorkspaceDirectory module.

Session keeps thin delegation methods so existing tests and callers see
the same surface; cursor errors are still mapped to SessionRequestError
at the session boundary.
This commit is contained in:
Mohamed Boudra
2026-05-02 15:34:19 +08:00
committed by GitHub
parent da85814940
commit 26737d3d70
2 changed files with 373 additions and 285 deletions
+26 -285
View File
@@ -31,7 +31,6 @@ import {
type ProjectPlacementPayload,
type WorkspaceSetupSnapshot,
type WorkspaceDescriptorPayload,
type WorkspaceStateBucket,
} from "./messages.js";
import type { TerminalManager } from "../terminal/terminal-manager.js";
import { TerminalSessionController } from "../terminal/terminal-session-controller.js";
@@ -196,6 +195,11 @@ import {
type GitHubService,
type PullRequestTimelineItem,
} from "../services/github-service.js";
import {
summarizeFetchWorkspacesEntries,
WorkspaceDirectory,
type WorkspaceUpdatesFilter,
} from "./workspace-directory.js";
import {
attemptFirstAgentBranchAutoName,
createPaseoWorktree,
@@ -420,12 +424,6 @@ function getFirstUserMessageText(timeline: readonly AgentTimelineItem[]): string
}
const FETCH_AGENTS_SORT_KEYS = ["status_priority", "created_at", "updated_at", "title"] as const;
const FETCH_WORKSPACES_SORT_KEYS = [
"status_priority",
"activity_at",
"name",
"project_id",
] as const;
export function resolveWaitForFinishError(options: {
status: "permission" | "error" | "idle";
@@ -484,7 +482,6 @@ type FetchWorkspacesRequestMessage = Extract<
{ type: "fetch_workspaces_request" }
>;
type FetchWorkspacesRequestFilter = NonNullable<FetchWorkspacesRequestMessage["filter"]>;
type FetchWorkspacesRequestSort = NonNullable<FetchWorkspacesRequestMessage["sort"]>[number];
type FetchWorkspacesResponsePayload = Extract<
SessionOutboundMessage,
{ type: "fetch_workspaces_response" }
@@ -495,7 +492,6 @@ type WorkspaceUpdatePayload = Extract<
SessionOutboundMessage,
{ type: "workspace_update" }
>["payload"];
type WorkspaceUpdatesFilter = FetchWorkspacesRequestFilter;
interface WorkspaceUpdatesSubscriptionState {
subscriptionId: string;
filter?: WorkspaceUpdatesFilter;
@@ -503,41 +499,6 @@ interface WorkspaceUpdatesSubscriptionState {
pendingUpdatesByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
lastEmittedByWorkspaceId: Map<string, WorkspaceUpdatePayload>;
}
function summarizeFetchWorkspacesEntries(entries: Iterable<FetchWorkspacesResponseEntry>): {
count: number;
projectIds: string[];
statusCounts: Record<string, number>;
workspaces: Array<{
id: string;
projectId: string;
projectDisplayName: string;
name: string;
status: FetchWorkspacesResponseEntry["status"];
workspaceKind: FetchWorkspacesResponseEntry["workspaceKind"];
activityAt: string | null;
}>;
} {
const workspaces = Array.from(entries, (entry) => ({
id: entry.id,
projectId: entry.projectId,
projectDisplayName: entry.projectDisplayName,
name: entry.name,
status: entry.status,
workspaceKind: entry.workspaceKind,
activityAt: entry.activityAt,
}));
const statusCounts = new Map<string, number>();
for (const workspace of workspaces) {
statusCounts.set(workspace.status, (statusCounts.get(workspace.status) ?? 0) + 1);
}
return {
count: workspaces.length,
projectIds: [...new Set(workspaces.map((workspace) => workspace.projectId))],
statusCounts: Object.fromEntries(statusCounts),
workspaces,
};
}
class SessionRequestError extends Error {
constructor(
@@ -872,7 +833,7 @@ export class Session {
private readonly workspaceSetupSnapshots: Map<string, WorkspaceSetupSnapshot>;
private readonly workspaceGitFetchSubscriptions = new Map<string, () => void>();
private readonly workspaceGitSubscriptions = new Map<string, () => void>();
private readonly archivingByWorkspaceId = new Map<string, string>();
private readonly workspaceDirectory: WorkspaceDirectory;
private registerVoiceSpeakHandler?: (agentId: string, handler: VoiceSpeakHandler) => void;
private unregisterVoiceSpeakHandler?: (agentId: string) => void;
private registerVoiceCallerContext?: (agentId: string, context: VoiceCallerContext) => void;
@@ -974,6 +935,14 @@ export class Session {
this.providerOverrides = providerOverrides;
this.isDev = isDev === true;
this.abortController = new AbortController();
this.workspaceDirectory = new WorkspaceDirectory({
logger: this.sessionLogger,
projectRegistry: this.projectRegistry,
workspaceRegistry: this.workspaceRegistry,
listAgentPayloads: () => this.listAgentPayloads(),
isProviderVisibleToClient: (provider) => this.isProviderVisibleToClient(provider),
buildWorkspaceDescriptor: (input) => this.buildWorkspaceDescriptor(input),
});
this.initializePerSessionManagers({ tts, stt, dictation });
@@ -5939,14 +5908,6 @@ export class Session {
};
}
private readonly workspaceStatePriority: Record<WorkspaceStateBucket, number> = {
needs_input: 0,
failed: 1,
running: 2,
attention: 3,
done: 4,
};
private readonly agentsPager = new SortablePager<
AgentSnapshotPayload,
FetchAgentsRequestSort["key"]
@@ -5969,28 +5930,6 @@ export class Session {
},
});
private readonly workspacesPager = new SortablePager<
WorkspaceDescriptorPayload,
FetchWorkspacesRequestSort["key"]
>({
validKeys: FETCH_WORKSPACES_SORT_KEYS,
defaultSort: [{ key: "activity_at", direction: "desc" }],
label: "fetch_workspaces",
getId: (workspace) => workspace.id,
getSortValue: (workspace, key) => {
switch (key) {
case "status_priority":
return this.workspaceStatePriority[workspace.status];
case "activity_at":
return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
case "name":
return workspace.name.toLocaleLowerCase();
case "project_id":
return workspace.projectId.toLocaleLowerCase();
}
},
});
private decodeAgentCursor(token: string, sort: SortSpec<FetchAgentsRequestSort["key"]>[]) {
try {
return this.agentsPager.decode(token, sort);
@@ -6002,37 +5941,6 @@ export class Session {
}
}
private decodeWorkspaceCursor(
token: string,
sort: SortSpec<FetchWorkspacesRequestSort["key"]>[],
) {
try {
return this.workspacesPager.decode(token, sort);
} catch (error) {
if (error instanceof CursorError) {
throw new SessionRequestError("invalid_cursor", error.message);
}
throw error;
}
}
private deriveWorkspaceStateBucket(agent: AgentSnapshotPayload): WorkspaceStateBucket {
const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
if (pendingPermissionCount > 0 || agent.attentionReason === "permission") {
return "needs_input";
}
if (agent.status === "error" || agent.attentionReason === "error") {
return "failed";
}
if (agent.status === "running") {
return "running";
}
if (agent.requiresAttention) {
return "attention";
}
return "done";
}
private async describeWorkspaceRecord(
workspace: PersistedWorkspaceRecord,
projectRecord?: PersistedProjectRecord | null,
@@ -6173,160 +6081,32 @@ export class Session {
}
markWorkspaceArchiving(workspaceIds: Iterable<string>, archivingAt: string): void {
for (const workspaceId of workspaceIds) {
this.archivingByWorkspaceId.set(workspaceId, archivingAt);
}
this.workspaceDirectory.markArchiving(workspaceIds, archivingAt);
}
clearWorkspaceArchiving(workspaceIds: Iterable<string>): void {
for (const workspaceId of workspaceIds) {
this.archivingByWorkspaceId.delete(workspaceId);
}
this.workspaceDirectory.clearArchiving(workspaceIds);
}
private async buildWorkspaceDescriptorMap(options: {
includeGitData: boolean;
workspaceIds?: Iterable<string>;
}): Promise<Map<string, WorkspaceDescriptorPayload>> {
const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
this.listAgentPayloads(),
this.workspaceRegistry.list(),
this.projectRegistry.list(),
]);
const activeProjects = new Map(
persistedProjects
.filter((project) => !project.archivedAt)
.map((project) => [project.projectId, project] as const),
);
const archivedProjectIds = new Set(
persistedProjects.filter((project) => project.archivedAt).map((project) => project.projectId),
);
const activeRecords = persistedWorkspaces.filter(
(workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId),
);
const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>();
const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
const workspaceIdsByDirectory = new Map(
activeRecords.map(
(workspace) =>
[normalizePersistedWorkspaceId(workspace.cwd), workspace.workspaceId] as const,
),
);
const includedWorkspaces = activeRecords.filter(
(workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId),
);
const workspaceDescriptors = await Promise.all(
includedWorkspaces.map((workspace) =>
this.buildWorkspaceDescriptor({
workspace,
projectRecord: activeProjects.get(workspace.projectId) ?? null,
includeGitData: options.includeGitData,
}),
),
);
for (let i = 0; i < includedWorkspaces.length; i += 1) {
const workspaceId = includedWorkspaces[i]!.workspaceId;
descriptorsByWorkspaceId.set(workspaceId, {
...workspaceDescriptors[i]!,
archivingAt: this.archivingByWorkspaceId.get(workspaceId) ?? null,
});
}
for (const agent of agents) {
if (agent.archivedAt) {
continue;
}
if (!this.isProviderVisibleToClient(agent.provider)) {
continue;
}
const workspaceId = workspaceIdsByDirectory.get(normalizePersistedWorkspaceId(agent.cwd));
if (workspaceId === undefined) {
continue;
}
const existing = descriptorsByWorkspaceId.get(workspaceId);
if (!existing) {
continue;
}
const bucket = this.deriveWorkspaceStateBucket(agent);
if (this.workspaceStatePriority[bucket] < this.workspaceStatePriority[existing.status]) {
existing.status = bucket;
}
}
return descriptorsByWorkspaceId;
return this.workspaceDirectory.buildDescriptorMap(options);
}
private resolveRegisteredWorkspaceIdForCwd(
cwd: string,
workspaces: PersistedWorkspaceRecord[],
): string {
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
if (exact) {
return exact.workspaceId;
}
const userHome = homedir();
let bestMatch: PersistedWorkspaceRecord | null = null;
for (const workspace of workspaces) {
if (workspace.cwd === userHome) continue;
if (workspace.archivedAt) continue;
const prefix = workspace.cwd.endsWith(sep) ? workspace.cwd : `${workspace.cwd}${sep}`;
if (!normalizedCwd.startsWith(prefix)) {
continue;
}
if (!bestMatch || workspace.cwd.length > bestMatch.cwd.length) {
bestMatch = workspace;
}
}
return bestMatch?.workspaceId ?? normalizedCwd;
}
private async listWorkspaceDescriptors(): Promise<WorkspaceDescriptorPayload[]> {
return Array.from(
(
await this.buildWorkspaceDescriptorMap({
includeGitData: true,
})
).values(),
);
return this.workspaceDirectory.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces);
}
private matchesWorkspaceFilter(input: {
workspace: WorkspaceDescriptorPayload;
filter: FetchWorkspacesRequestFilter | undefined;
}): boolean {
const { workspace, filter } = input;
if (!filter) {
return true;
}
if (filter.projectId && filter.projectId.trim().length > 0) {
if (workspace.projectId !== filter.projectId.trim()) {
return false;
}
}
if (filter.idPrefix && filter.idPrefix.trim().length > 0) {
if (!String(workspace.id).startsWith(filter.idPrefix.trim())) {
return false;
}
}
if (filter.query && filter.query.trim().length > 0) {
const query = filter.query.trim().toLocaleLowerCase();
const haystacks = [workspace.name, String(workspace.projectId), String(workspace.id)];
if (!haystacks.some((value) => value.toLocaleLowerCase().includes(query))) {
return false;
}
}
return true;
return this.workspaceDirectory.matchesFilter(input);
}
private async listFetchWorkspacesEntries(
@@ -6335,53 +6115,14 @@ export class Session {
entries: FetchWorkspacesResponseEntry[];
pageInfo: FetchWorkspacesResponsePageInfo;
}> {
const filter = request.filter;
const sort = this.workspacesPager.normalizeSort(request.sort);
let entries = await this.listWorkspaceDescriptors();
const listedCount = entries.length;
entries = entries.filter((workspace) => this.matchesWorkspaceFilter({ workspace, filter }));
const filteredCount = entries.length;
entries.sort((left, right) => this.workspacesPager.compare(left, right, sort));
const cursorToken = request.page?.cursor;
if (cursorToken) {
const cursor = this.decodeWorkspaceCursor(cursorToken, sort);
entries = entries.filter(
(workspace) => this.workspacesPager.compareWithCursor(workspace, cursor, sort) > 0,
);
try {
return await this.workspaceDirectory.listFetchEntries(request);
} catch (error) {
if (error instanceof CursorError) {
throw new SessionRequestError("invalid_cursor", error.message);
}
throw error;
}
const limit = request.page?.limit ?? 200;
const pagedEntries = entries.slice(0, limit);
const hasMore = entries.length > limit;
const nextCursor =
hasMore && pagedEntries.length > 0
? this.workspacesPager.encode(pagedEntries[pagedEntries.length - 1], sort)
: null;
this.sessionLogger.debug(
{
requestId: request.requestId,
filter: request.filter ?? null,
sort,
page: request.page ?? null,
listedCount,
filteredCount,
returnedCount: pagedEntries.length,
hasMore,
nextCursor,
},
"fetch_workspaces_entries_listed",
);
return {
entries: pagedEntries,
pageInfo: {
nextCursor,
prevCursor: request.page?.cursor ?? null,
hasMore,
},
};
}
private bufferOrEmitWorkspaceUpdate(
@@ -0,0 +1,347 @@
import { homedir } from "node:os";
import { sep } from "node:path";
import type pino from "pino";
import type {
AgentSnapshotPayload,
SessionInboundMessage,
SessionOutboundMessage,
WorkspaceDescriptorPayload,
WorkspaceStateBucket,
} from "./messages.js";
import { SortablePager } from "./pagination/sortable-pager.js";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
const FETCH_WORKSPACES_SORT_KEYS = [
"status_priority",
"activity_at",
"name",
"project_id",
] as const;
type FetchWorkspacesRequestMessage = Extract<
SessionInboundMessage,
{ type: "fetch_workspaces_request" }
>;
type FetchWorkspacesRequestFilter = NonNullable<FetchWorkspacesRequestMessage["filter"]>;
type FetchWorkspacesRequestSort = NonNullable<FetchWorkspacesRequestMessage["sort"]>[number];
type FetchWorkspacesResponsePayload = Extract<
SessionOutboundMessage,
{ type: "fetch_workspaces_response" }
>["payload"];
type FetchWorkspacesResponseEntry = FetchWorkspacesResponsePayload["entries"][number];
type FetchWorkspacesResponsePageInfo = FetchWorkspacesResponsePayload["pageInfo"];
export type WorkspaceUpdatesFilter = FetchWorkspacesRequestFilter;
export interface WorkspaceDirectoryDeps {
logger: pino.Logger;
projectRegistry: {
list(): Promise<PersistedProjectRecord[]>;
};
workspaceRegistry: {
list(): Promise<PersistedWorkspaceRecord[]>;
};
listAgentPayloads(): Promise<AgentSnapshotPayload[]>;
isProviderVisibleToClient(provider: string): boolean;
buildWorkspaceDescriptor(input: {
workspace: PersistedWorkspaceRecord;
projectRecord?: PersistedProjectRecord | null;
includeGitData: boolean;
}): Promise<WorkspaceDescriptorPayload>;
}
export function summarizeFetchWorkspacesEntries(entries: Iterable<FetchWorkspacesResponseEntry>): {
count: number;
projectIds: string[];
statusCounts: Record<string, number>;
workspaces: Array<{
id: string;
projectId: string;
projectDisplayName: string;
name: string;
status: FetchWorkspacesResponseEntry["status"];
workspaceKind: FetchWorkspacesResponseEntry["workspaceKind"];
activityAt: string | null;
}>;
} {
const workspaces = Array.from(entries, (entry) => ({
id: entry.id,
projectId: entry.projectId,
projectDisplayName: entry.projectDisplayName,
name: entry.name,
status: entry.status,
workspaceKind: entry.workspaceKind,
activityAt: entry.activityAt,
}));
const statusCounts = new Map<string, number>();
for (const workspace of workspaces) {
statusCounts.set(workspace.status, (statusCounts.get(workspace.status) ?? 0) + 1);
}
return {
count: workspaces.length,
projectIds: [...new Set(workspaces.map((workspace) => workspace.projectId))],
statusCounts: Object.fromEntries(statusCounts),
workspaces,
};
}
export class WorkspaceDirectory {
private readonly archivingByWorkspaceId = new Map<string, string>();
private readonly workspaceStatePriority: Record<WorkspaceStateBucket, number> = {
needs_input: 0,
failed: 1,
running: 2,
attention: 3,
done: 4,
};
private readonly pager = new SortablePager<
WorkspaceDescriptorPayload,
FetchWorkspacesRequestSort["key"]
>({
validKeys: FETCH_WORKSPACES_SORT_KEYS,
defaultSort: [{ key: "activity_at", direction: "desc" }],
label: "fetch_workspaces",
getId: (workspace) => workspace.id,
getSortValue: (workspace, key) => {
switch (key) {
case "status_priority":
return this.workspaceStatePriority[workspace.status];
case "activity_at":
return workspace.activityAt ? Date.parse(workspace.activityAt) : null;
case "name":
return workspace.name.toLocaleLowerCase();
case "project_id":
return workspace.projectId.toLocaleLowerCase();
}
},
});
constructor(private readonly deps: WorkspaceDirectoryDeps) {}
markArchiving(workspaceIds: Iterable<string>, archivingAt: string): void {
for (const workspaceId of workspaceIds) {
this.archivingByWorkspaceId.set(workspaceId, archivingAt);
}
}
clearArchiving(workspaceIds: Iterable<string>): void {
for (const workspaceId of workspaceIds) {
this.archivingByWorkspaceId.delete(workspaceId);
}
}
async buildDescriptorMap(options: {
includeGitData: boolean;
workspaceIds?: Iterable<string>;
}): Promise<Map<string, WorkspaceDescriptorPayload>> {
const [agents, persistedWorkspaces, persistedProjects] = await Promise.all([
this.deps.listAgentPayloads(),
this.deps.workspaceRegistry.list(),
this.deps.projectRegistry.list(),
]);
const activeProjects = new Map(
persistedProjects
.filter((project) => !project.archivedAt)
.map((project) => [project.projectId, project] as const),
);
const archivedProjectIds = new Set(
persistedProjects.filter((project) => project.archivedAt).map((project) => project.projectId),
);
const activeRecords = persistedWorkspaces.filter(
(workspace) => !workspace.archivedAt && !archivedProjectIds.has(workspace.projectId),
);
const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>();
const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
const workspaceIdsByDirectory = new Map(
activeRecords.map(
(workspace) => [normalizeWorkspaceId(workspace.cwd), workspace.workspaceId] as const,
),
);
const includedWorkspaces = activeRecords.filter(
(workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId),
);
const workspaceDescriptors = await Promise.all(
includedWorkspaces.map((workspace) =>
this.deps.buildWorkspaceDescriptor({
workspace,
projectRecord: activeProjects.get(workspace.projectId) ?? null,
includeGitData: options.includeGitData,
}),
),
);
for (let i = 0; i < includedWorkspaces.length; i += 1) {
const workspaceId = includedWorkspaces[i]!.workspaceId;
descriptorsByWorkspaceId.set(workspaceId, {
...workspaceDescriptors[i]!,
archivingAt: this.archivingByWorkspaceId.get(workspaceId) ?? null,
});
}
for (const agent of agents) {
if (agent.archivedAt) {
continue;
}
if (!this.deps.isProviderVisibleToClient(agent.provider)) {
continue;
}
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(agent.cwd));
if (workspaceId === undefined) {
continue;
}
const existing = descriptorsByWorkspaceId.get(workspaceId);
if (!existing) {
continue;
}
const bucket = this.deriveStateBucket(agent);
if (this.workspaceStatePriority[bucket] < this.workspaceStatePriority[existing.status]) {
existing.status = bucket;
}
}
return descriptorsByWorkspaceId;
}
resolveRegisteredWorkspaceIdForCwd(cwd: string, workspaces: PersistedWorkspaceRecord[]): string {
const normalizedCwd = normalizeWorkspaceId(cwd);
const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
if (exact) {
return exact.workspaceId;
}
const userHome = homedir();
let bestMatch: PersistedWorkspaceRecord | null = null;
for (const workspace of workspaces) {
if (workspace.cwd === userHome) continue;
if (workspace.archivedAt) continue;
const prefix = workspace.cwd.endsWith(sep) ? workspace.cwd : `${workspace.cwd}${sep}`;
if (!normalizedCwd.startsWith(prefix)) {
continue;
}
if (!bestMatch || workspace.cwd.length > bestMatch.cwd.length) {
bestMatch = workspace;
}
}
return bestMatch?.workspaceId ?? normalizedCwd;
}
async listDescriptors(): Promise<WorkspaceDescriptorPayload[]> {
return Array.from(
(
await this.buildDescriptorMap({
includeGitData: true,
})
).values(),
);
}
matchesFilter(input: {
workspace: WorkspaceDescriptorPayload;
filter: FetchWorkspacesRequestFilter | undefined;
}): boolean {
const { workspace, filter } = input;
if (!filter) {
return true;
}
if (filter.projectId && filter.projectId.trim().length > 0) {
if (workspace.projectId !== filter.projectId.trim()) {
return false;
}
}
if (filter.idPrefix && filter.idPrefix.trim().length > 0) {
if (!String(workspace.id).startsWith(filter.idPrefix.trim())) {
return false;
}
}
if (filter.query && filter.query.trim().length > 0) {
const query = filter.query.trim().toLocaleLowerCase();
const haystacks = [workspace.name, String(workspace.projectId), String(workspace.id)];
if (!haystacks.some((value) => value.toLocaleLowerCase().includes(query))) {
return false;
}
}
return true;
}
async listFetchEntries(request: FetchWorkspacesRequestMessage): Promise<{
entries: FetchWorkspacesResponseEntry[];
pageInfo: FetchWorkspacesResponsePageInfo;
}> {
const filter = request.filter;
const sort = this.pager.normalizeSort(request.sort);
let entries = await this.listDescriptors();
const listedCount = entries.length;
entries = entries.filter((workspace) => this.matchesFilter({ workspace, filter }));
const filteredCount = entries.length;
entries.sort((left, right) => this.pager.compare(left, right, sort));
const cursorToken = request.page?.cursor;
if (cursorToken) {
const cursor = this.pager.decode(cursorToken, sort);
entries = entries.filter(
(workspace) => this.pager.compareWithCursor(workspace, cursor, sort) > 0,
);
}
const limit = request.page?.limit ?? 200;
const pagedEntries = entries.slice(0, limit);
const hasMore = entries.length > limit;
const nextCursor =
hasMore && pagedEntries.length > 0
? this.pager.encode(pagedEntries[pagedEntries.length - 1], sort)
: null;
this.deps.logger.debug(
{
requestId: request.requestId,
filter: request.filter ?? null,
sort,
page: request.page ?? null,
listedCount,
filteredCount,
returnedCount: pagedEntries.length,
hasMore,
nextCursor,
},
"fetch_workspaces_entries_listed",
);
return {
entries: pagedEntries,
pageInfo: {
nextCursor,
prevCursor: request.page?.cursor ?? null,
hasMore,
},
};
}
private deriveStateBucket(agent: AgentSnapshotPayload): WorkspaceStateBucket {
const pendingPermissionCount = agent.pendingPermissions?.length ?? 0;
if (pendingPermissionCount > 0 || agent.attentionReason === "permission") {
return "needs_input";
}
if (agent.status === "error" || agent.attentionReason === "error") {
return "failed";
}
if (agent.status === "running") {
return "running";
}
if (agent.requiresAttention) {
return "attention";
}
return "done";
}
}