authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 18:32:38-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 18:53:21-08:00
logb981fad920f43fe64dac4b547aba6ca4fb2f4715
tree0ccb568522239d235bba0a8f071285fb7bab4a92
parent8a2f2a5c2b057f62473172dae3e2467afb8db394

Sync thread metadata after worktree branch rename

- Dispatch `thread.meta.update` with the renamed branch after `git.renameBranch` - Remove pre-rename active-branch status check and related skip path - Update reactor tests to assert branch metadata moves to generated branch

2 files changed, 23 insertions(+), 111 deletions(-)

apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+12-75
......@@ -108,27 +108,11 @@ describe("ProviderCommandReactor", () => {
108108 const interruptTurn = vi.fn((_: unknown) => Effect.void);
109109 const respondToRequest = vi.fn((_: unknown) => Effect.void);
110110 const stopSession = vi.fn((_: unknown) => Effect.void);
111 const statusBranchByCwd = new Map<string, string | null>();
112111 const renameBranch = vi.fn((_: unknown) =>
113112 Effect.succeed({
114113 branch: "t3code/generated-name",
115114 }),
116115 );
117 const statusDetails = vi.fn((cwd: string) =>
118 Effect.succeed({
119 branch: statusBranchByCwd.get(cwd) ?? null,
120 upstreamRef: null,
121 hasWorkingTreeChanges: false,
122 workingTree: {
123 files: [],
124 insertions: 0,
125 deletions: 0,
126 },
127 hasUpstream: false,
128 aheadCount: 0,
129 behindCount: 0,
130 }),
131 );
132116 const generateBranchName = vi.fn<TextGenerationShape["generateBranchName"]>(() =>
133117 Effect.succeed({
134118 branch: "generated-name",
......@@ -157,12 +141,7 @@ describe("ProviderCommandReactor", () => {
157141 const layer = ProviderCommandReactorLive.pipe(
158142 Layer.provideMerge(orchestrationLayer),
159143 Layer.provideMerge(Layer.succeed(ProviderService, service)),
160 Layer.provideMerge(
161 Layer.succeed(
162 GitCore,
163 { renameBranch, statusDetails } as unknown as GitCoreShape,
164 ),
165 ),
144 Layer.provideMerge(Layer.succeed(GitCore, { renameBranch } as unknown as GitCoreShape)),
166145 Layer.provideMerge(
167146 Layer.succeed(TextGeneration, { generateBranchName } as unknown as TextGenerationShape),
168147 ),
......@@ -210,11 +189,7 @@ describe("ProviderCommandReactor", () => {
210189 respondToRequest,
211190 stopSession,
212191 renameBranch,
213 statusDetails,
214192 generateBranchName,
215 setStatusBranch: (cwd: string, branch: string | null) => {
216 statusBranchByCwd.set(cwd, branch);
217 },
218193 stateDir,
219194 };
220195 }
......@@ -260,7 +235,6 @@ describe("ProviderCommandReactor", () => {
260235 it("generates and renames temporary worktree branch on first turn", async () => {
261236 const harness = await createHarness();
262237 const now = new Date().toISOString();
263 const worktreeCwd = "/tmp/provider-project/.t3/worktrees/t3code-89abc123";
264238
265239 await Effect.runPromise(
266240 harness.engine.dispatch({
......@@ -268,10 +242,9 @@ describe("ProviderCommandReactor", () => {
268242 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch"),
269243 threadId: ThreadId.makeUnsafe("thread-1"),
270244 branch: "t3code/89abc123",
271 worktreePath: worktreeCwd,
245 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
272246 }),
273247 );
274 harness.setStatusBranch(worktreeCwd, "t3code/89abc123");
275248
276249 await Effect.runPromise(
277250 harness.engine.dispatch({
......@@ -302,7 +275,7 @@ describe("ProviderCommandReactor", () => {
302275 await waitFor(() => harness.renameBranch.mock.calls.length === 1);
303276
304277 expect(harness.generateBranchName.mock.calls[0]?.[0]).toEqual({
305 cwd: worktreeCwd,
278 cwd: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
306279 message: "Fix visual bug from screenshot",
307280 attachments: [
308281 {
......@@ -315,14 +288,18 @@ describe("ProviderCommandReactor", () => {
315288 ],
316289 });
317290 expect(harness.renameBranch.mock.calls[0]?.[0]).toEqual({
318 cwd: worktreeCwd,
291 cwd: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
319292 oldBranch: "t3code/89abc123",
320293 newBranch: "t3code/generated-name",
321294 });
322295
323 const readModel = await Effect.runPromise(harness.engine.getReadModel());
324 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));
325 expect(thread?.branch).toBe("t3code/89abc123");
296 await waitFor(() => {
297 const readModel = Effect.runSync(harness.engine.getReadModel());
298 const thread = readModel.threads.find(
299 (entry) => entry.id === ThreadId.makeUnsafe("thread-1"),
300 );
301 return thread?.branch === "t3code/generated-name";
302 });
326303 });
327304
328305 it("passes persisted attachment references to branch generation and turn start", async () => {
......@@ -396,7 +373,6 @@ describe("ProviderCommandReactor", () => {
396373 it("skips worktree branch generation after the first user turn", async () => {
397374 const harness = await createHarness();
398375 const now = new Date().toISOString();
399 const worktreeCwd = "/tmp/provider-project/.t3/worktrees/t3code-1234abcd";
400376
401377 await Effect.runPromise(
402378 harness.engine.dispatch({
......@@ -404,10 +380,9 @@ describe("ProviderCommandReactor", () => {
404380 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-2"),
405381 threadId: ThreadId.makeUnsafe("thread-1"),
406382 branch: "t3code/1234abcd",
407 worktreePath: worktreeCwd,
383 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-1234abcd",
408384 }),
409385 );
410 harness.setStatusBranch(worktreeCwd, "t3code/1234abcd");
411386
412387 await Effect.runPromise(
413388 harness.engine.dispatch({
......@@ -450,44 +425,6 @@ describe("ProviderCommandReactor", () => {
450425 expect(harness.renameBranch.mock.calls.length).toBe(1);
451426 });
452427
453 it("skips worktree rename when active branch already changed", async () => {
454 const harness = await createHarness();
455 const now = new Date().toISOString();
456 const worktreeCwd = "/tmp/provider-project/.t3/worktrees/t3code-badf00d0";
457
458 await Effect.runPromise(
459 harness.engine.dispatch({
460 type: "thread.meta.update",
461 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-switched"),
462 threadId: ThreadId.makeUnsafe("thread-1"),
463 branch: "t3code/badf00d0",
464 worktreePath: worktreeCwd,
465 }),
466 );
467 harness.setStatusBranch(worktreeCwd, "feature/add-vscode-open-in-support");
468
469 await Effect.runPromise(
470 harness.engine.dispatch({
471 type: "thread.turn.start",
472 commandId: CommandId.makeUnsafe("cmd-turn-start-worktree-switched"),
473 threadId: ThreadId.makeUnsafe("thread-1"),
474 message: {
475 messageId: asMessageId("user-message-worktree-switched"),
476 role: "user",
477 text: "Fix visual bug from screenshot",
478 attachments: [],
479 },
480 approvalPolicy: "on-request",
481 sandboxMode: "workspace-write",
482 createdAt: now,
483 }),
484 );
485
486 await waitFor(() => harness.generateBranchName.mock.calls.length === 1);
487 await Effect.runPromise(Effect.sleep("20 millis"));
488 expect(harness.renameBranch.mock.calls.length).toBe(0);
489 });
490
491428 it("skips worktree rename when branch-name generation fails", async () => {
492429 const harness = await createHarness();
493430 const now = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+11-36
......@@ -330,42 +330,17 @@ const make = Effect.gen(function* () {
330330 const targetBranch = buildGeneratedWorktreeBranchName(generated.branch);
331331 if (targetBranch === oldBranch) return Effect.void;
332332
333 return Effect.gen(function* () {
334 const currentBranch = yield* git.statusDetails(cwd).pipe(
335 Effect.map((status) => status.branch),
336 Effect.catch((error) =>
337 Effect.logWarning(
338 "provider command reactor failed to read active worktree branch",
339 {
340 threadId: input.threadId,
341 cwd,
342 oldBranch,
343 targetBranch,
344 reason: error.message,
345 },
346 ).pipe(Effect.as(null)),
347 ),
348 );
349
350 if (currentBranch !== oldBranch) {
351 return yield* Effect.logWarning(
352 "provider command reactor skipped worktree branch rename because active branch changed",
353 {
354 threadId: input.threadId,
355 cwd,
356 oldBranch,
357 targetBranch,
358 currentBranch,
359 },
360 );
361 }
362
363 yield* git.renameBranch({
364 cwd,
365 oldBranch,
366 newBranch: targetBranch,
367 });
368 });
333 return Effect.flatMap(
334 git.renameBranch({ cwd, oldBranch, newBranch: targetBranch }),
335 (renamed) =>
336 orchestrationEngine.dispatch({
337 type: "thread.meta.update",
338 commandId: serverCommandId("worktree-branch-rename"),
339 threadId: input.threadId,
340 branch: renamed.branch,
341 worktreePath: cwd,
342 }),
343 );
369344 }),
370345 Effect.catchCause((cause) =>
371346 Effect.logWarning(