perf(sidebar): index session ownership and narrow live subscriptions

Replace repeated project-by-session directory matching across sidebar hooks with a shared ownership index that resolves each unique directory once and exposes direct project and folder-scope buckets.

Gate destructive folder reconciliation on authoritative session data and topology readiness, preserve last-known worktrees after discovery failures, and retain nested-project, VS Code, active/archive dedupe, and Windows drive-root semantics.

Narrow cross-directory subscriptions to session and status slices so streaming deltas no longer trigger global aggregation. Reuse a cached session ID index for permission lineage checks instead of rebuilding it on every session switch.

On the reported 15-project, 67-worktree, 14,561-session shape, ownership indexing averages 3.81 ms versus roughly 450 ms for the cache-only hotfix.

Validation: 28 targeted tests, UI type-check, UI lint, and dead-code analysis.
This commit is contained in:
Bohdan Triapitsyn
2026-07-13 23:18:12 +03:00
parent 799904f0f4
commit b36afbf5ee
19 changed files with 619 additions and 511 deletions
@@ -60,6 +60,17 @@ describe("autoRespondsPermission", () => {
})).toBe(true)
})
test("uses a prebuilt session index for lineage lookup", () => {
const parent = makeSession("parent")
const child = makeSession("child", "parent")
expect(autoRespondsPermission({
autoAccept: { parent: true },
sessions: [],
sessionById: new Map([[parent.id, parent], [child.id, child]]),
sessionID: "child",
})).toBe(true)
})
test("returns false when only sibling has autoAccept enabled", () => {
const autoAccept: PermissionAutoAcceptMap = { sibling: true }
const sessions = [
@@ -10,8 +10,12 @@ const buildSessionMap = (sessions: Session[]): Map<string, Session> => {
return map;
};
const resolveLineage = (sessionID: string, sessions: Session[]): string[] => {
const map = buildSessionMap(sessions);
const resolveLineage = (
sessionID: string,
sessions: Session[],
sessionById?: ReadonlyMap<string, Session>,
): string[] => {
const map = sessionById ?? buildSessionMap(sessions);
const result: string[] = [];
const seen = new Set<string>();
let current: string | undefined = sessionID;
@@ -28,10 +32,12 @@ const resolveLineage = (sessionID: string, sessions: Session[]): string[] => {
export const autoRespondsPermission = (input: {
autoAccept: PermissionAutoAcceptMap;
sessions: Session[];
sessionById?: ReadonlyMap<string, Session>;
sessionID: string;
}): boolean => {
const { autoAccept, sessions, sessionID } = input;
const lineage = resolveLineage(sessionID, sessions);
const { autoAccept, sessions, sessionById, sessionID } = input;
if (Object.keys(autoAccept).length === 0) return false;
const lineage = resolveLineage(sessionID, sessions, sessionById);
for (const id of lineage) {
if (!Object.prototype.hasOwnProperty.call(autoAccept, id)) {