Skip to content

Commit 6eba8df

Browse files
committed
Fix project registry: key by cwd, defer registration until session succeeds
- registerProject now finds existing entries by cwd (not name), so two repos with the same name get separate entries - removeProject takes cwd instead of name for unambiguous deletion - Server DELETE /daemon/projects now accepts JSON body with cwd - Session factory defers registerProject until after session creation succeeds, avoiding phantom entries from failed requests - Hub-client: add missing scheduleReconnect after protocol error frames
1 parent 60eb1d2 commit 6eba8df

7 files changed

Lines changed: 54 additions & 29 deletions

File tree

apps/debug-frontend/src/daemon/events/hub-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export class DaemonHubClient {
221221
this.socket = undefined;
222222
this.daemonSubscribed = false;
223223
socket?.close();
224+
this.scheduleReconnect();
224225
return;
225226
}
226227
if (

apps/frontend/src/daemon/api/client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export interface DaemonApiClient {
6464
cwd: string,
6565
name?: string,
6666
): Promise<DaemonApiResult<{ ok: true; project: ProjectEntry }>>;
67-
removeProject(name: string): Promise<DaemonApiResult<{ ok: true }>>;
67+
removeProject(cwd: string): Promise<DaemonApiResult<{ ok: true }>>;
6868
createReviewSession(cwd: string): Promise<DaemonApiResult<SessionResponse>>;
6969
createArchiveSession(cwd: string): Promise<DaemonApiResult<SessionResponse>>;
7070
}
@@ -442,12 +442,12 @@ export function createDaemonApiClient(options: DaemonApiClientOptions = {}): Dae
442442
);
443443
},
444444

445-
removeProject(name) {
445+
removeProject(cwd) {
446446
return requestJson(
447447
fetchImpl,
448-
joinUrl(options.baseUrl, `/daemon/projects/${encodeURIComponent(name)}`),
448+
joinUrl(options.baseUrl, "/daemon/projects"),
449449
isDeleteSessionResponse,
450-
{ method: "DELETE" },
450+
{ method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cwd }) },
451451
);
452452
},
453453

apps/frontend/src/stores/project-store.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ProjectStoreActions {
1818
name?: string,
1919
client?: DaemonApiClient,
2020
): Promise<ProjectEntry | undefined>;
21-
removeProject(name: string, client?: DaemonApiClient): Promise<boolean>;
21+
removeProject(cwd: string, client?: DaemonApiClient): Promise<boolean>;
2222
}
2323

2424
export type ProjectStore = ProjectStoreState & ProjectStoreActions;
@@ -60,7 +60,7 @@ export function createProjectStore(initial: Partial<ProjectStoreState> = {}) {
6060
}
6161
const entry = result.data.project;
6262
set((state) => {
63-
const idx = state.projects.findIndex((p) => p.name === entry.name);
63+
const idx = state.projects.findIndex((p) => p.cwd === entry.cwd);
6464
if (idx >= 0) {
6565
state.projects[idx] = entry;
6666
} else {
@@ -70,16 +70,16 @@ export function createProjectStore(initial: Partial<ProjectStoreState> = {}) {
7070
return entry;
7171
},
7272

73-
async removeProject(name, client = daemonApiClient) {
74-
const result = await client.removeProject(name);
73+
async removeProject(cwd, client = daemonApiClient) {
74+
const result = await client.removeProject(cwd);
7575
if (!result.ok) {
7676
set((state) => {
7777
state.error = result.error.message;
7878
});
7979
return false;
8080
}
8181
set((state) => {
82-
state.projects = state.projects.filter((p) => p.name !== name);
82+
state.projects = state.projects.filter((p) => p.cwd !== cwd);
8383
});
8484
return true;
8585
},

packages/server/daemon/project-registry.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,32 @@ describe("project-registry", () => {
3333
expect(entries[0].cwd).toBe("/tmp/test");
3434
});
3535

36-
it("upserts on same name", () => {
36+
it("upserts on same cwd, updating name", () => {
37+
registerProject("proj-old", "/path/a", { baseDir });
38+
registerProject("proj-new", "/path/a", { baseDir });
39+
const entries = readProjectRegistry({ baseDir });
40+
expect(entries).toHaveLength(1);
41+
expect(entries[0].name).toBe("proj-new");
42+
expect(entries[0].cwd).toBe("/path/a");
43+
});
44+
45+
it("creates separate entries for same name, different cwd", () => {
3746
registerProject("proj", "/path/a", { baseDir });
3847
registerProject("proj", "/path/b", { baseDir });
3948
const entries = readProjectRegistry({ baseDir });
40-
expect(entries).toHaveLength(1);
41-
expect(entries[0].cwd).toBe("/path/b");
49+
expect(entries).toHaveLength(2);
4250
});
4351

44-
it("removes a project", () => {
52+
it("removes a project by cwd", () => {
4553
registerProject("a", "/a", { baseDir });
4654
registerProject("b", "/b", { baseDir });
47-
expect(removeProject("a", { baseDir })).toBe(true);
55+
expect(removeProject("/a", { baseDir })).toBe(true);
4856
expect(readProjectRegistry({ baseDir })).toHaveLength(1);
4957
expect(readProjectRegistry({ baseDir })[0].name).toBe("b");
5058
});
5159

52-
it("returns false when removing nonexistent project", () => {
53-
expect(removeProject("nope", { baseDir })).toBe(false);
60+
it("returns false when removing nonexistent cwd", () => {
61+
expect(removeProject("/nope", { baseDir })).toBe(false);
5462
});
5563

5664
it("lists sorted by lastSeen descending", async () => {

packages/server/daemon/project-registry.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export function registerProject(
5151
): DaemonProjectEntry {
5252
const entries = readProjectRegistry(options);
5353
const now = new Date().toISOString();
54-
const existing = entries.find((e) => e.name === name);
54+
const existing = entries.find((e) => e.cwd === cwd);
5555
if (existing) {
56-
existing.cwd = cwd;
56+
existing.name = name;
5757
existing.lastSeen = now;
5858
writeProjectRegistry(entries, options);
5959
return existing;
@@ -65,11 +65,11 @@ export function registerProject(
6565
}
6666

6767
export function removeProject(
68-
name: string,
68+
cwd: string,
6969
options: ProjectRegistryOptions = {},
7070
): boolean {
7171
const entries = readProjectRegistry(options);
72-
const filtered = entries.filter((e) => e.name !== name);
72+
const filtered = entries.filter((e) => e.cwd !== cwd);
7373
if (filtered.length === entries.length) return false;
7474
writeProjectRegistry(filtered, options);
7575
return true;

packages/server/daemon/server.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,12 +483,22 @@ export function createDaemonFetchHandler(options: DaemonServerOptions): DaemonFe
483483
}
484484
}
485485

486-
const projectRoute = url.pathname.match(/^\/daemon\/projects\/([^/]+)$/);
487-
if (projectRoute && req.method === "DELETE") {
488-
const name = decodeURIComponent(projectRoute[1]);
489-
const removed = removeProject(name);
486+
if (url.pathname === "/daemon/projects" && req.method === "DELETE") {
487+
if (!isJsonRequest(req)) {
488+
return json(createDaemonErrorResponse("invalid-request", "Project requests must use application/json."), { status: 415 });
489+
}
490+
let body: { cwd?: unknown };
491+
try {
492+
body = await req.json() as { cwd?: unknown };
493+
} catch {
494+
return json(createDaemonErrorResponse("invalid-request", "Invalid project request JSON."), { status: 400 });
495+
}
496+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
497+
return json(createDaemonErrorResponse("invalid-request", "Project removal requires a cwd path."), { status: 400 });
498+
}
499+
const removed = removeProject(body.cwd);
490500
if (!removed) {
491-
return json(createDaemonErrorResponse("invalid-request", `Project not found: ${name}`), { status: 404 });
501+
return json(createDaemonErrorResponse("invalid-request", `Project not found: ${body.cwd}`), { status: 404 });
492502
}
493503
return json({ ok: true });
494504
}

packages/server/daemon/session-factory.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,13 @@ export function createDaemonSessionFactory(options: DaemonSessionFactoryOptions)
489489
const request = createRequest.request;
490490
const cwd = getRequestCwd(request);
491491
const project = (await detectProjectName(cwd)) ?? "_unknown";
492-
try {
493-
const tmp = tmpdir();
494-
if (!cwd.startsWith(tmp)) registerProject(project, cwd);
495-
} catch {}
496492
const id = createDaemonSessionId();
497493
const url = makeSessionUrl(context.endpoint.baseUrl, id);
494+
const autoRegister = () => {
495+
try {
496+
if (!cwd.startsWith(tmpdir())) registerProject(project, cwd);
497+
} catch {}
498+
};
498499
const ttlMs = request.timeoutMs === null
499500
? undefined
500501
: request.timeoutMs !== undefined
@@ -537,6 +538,7 @@ export function createDaemonSessionFactory(options: DaemonSessionFactoryOptions)
537538
dispose: registerSessionDecision(context, id, () => session.waitForDecision(), () => session.dispose()),
538539
remoteShare,
539540
});
541+
autoRegister();
540542
return record;
541543
}
542544

@@ -570,6 +572,7 @@ export function createDaemonSessionFactory(options: DaemonSessionFactoryOptions)
570572
() => ({ opened: true }),
571573
),
572574
});
575+
autoRegister();
573576
return record;
574577
}
575578

@@ -607,6 +610,7 @@ export function createDaemonSessionFactory(options: DaemonSessionFactoryOptions)
607610
})),
608611
remoteShare,
609612
});
613+
autoRegister();
610614
return record;
611615
}
612616

@@ -659,6 +663,7 @@ export function createDaemonSessionFactory(options: DaemonSessionFactoryOptions)
659663
dispose: registerSessionDecision(context, id, () => session.waitForDecision(), () => session.dispose()),
660664
remoteShare,
661665
});
666+
autoRegister();
662667
return record;
663668
}
664669

@@ -683,6 +688,7 @@ export function createDaemonSessionFactory(options: DaemonSessionFactoryOptions)
683688
handleRequest: session.handleRequest,
684689
dispose: registerSessionDecision(context, id, () => session.waitForDecision(), () => session.dispose()),
685690
});
691+
autoRegister();
686692
return record;
687693
}
688694

0 commit comments

Comments
 (0)