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", () => {...@@ -108,27 +108,11 @@ describe("ProviderCommandReactor", () => {
108 const interruptTurn = vi.fn((_: unknown) => Effect.void);108 const interruptTurn = vi.fn((_: unknown) => Effect.void);
109 const respondToRequest = vi.fn((_: unknown) => Effect.void);109 const respondToRequest = vi.fn((_: unknown) => Effect.void);
110 const stopSession = vi.fn((_: unknown) => Effect.void);110 const stopSession = vi.fn((_: unknown) => Effect.void);
111 const statusBranchByCwd = new Map<string, string | null>();
112 const renameBranch = vi.fn((_: unknown) =>111 const renameBranch = vi.fn((_: unknown) =>
113 Effect.succeed({112 Effect.succeed({
114 branch: "t3code/generated-name",113 branch: "t3code/generated-name",
115 }),114 }),
116 );115 );
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 );
132 const generateBranchName = vi.fn<TextGenerationShape["generateBranchName"]>(() =>116 const generateBranchName = vi.fn<TextGenerationShape["generateBranchName"]>(() =>
133 Effect.succeed({117 Effect.succeed({
134 branch: "generated-name",118 branch: "generated-name",
...@@ -157,12 +141,7 @@ describe("ProviderCommandReactor", () => {...@@ -157,12 +141,7 @@ describe("ProviderCommandReactor", () => {
157 const layer = ProviderCommandReactorLive.pipe(141 const layer = ProviderCommandReactorLive.pipe(
158 Layer.provideMerge(orchestrationLayer),142 Layer.provideMerge(orchestrationLayer),
159 Layer.provideMerge(Layer.succeed(ProviderService, service)),143 Layer.provideMerge(Layer.succeed(ProviderService, service)),
160 Layer.provideMerge(144 Layer.provideMerge(Layer.succeed(GitCore, { renameBranch } as unknown as GitCoreShape)),
161 Layer.succeed(
162 GitCore,
163 { renameBranch, statusDetails } as unknown as GitCoreShape,
164 ),
165 ),
166 Layer.provideMerge(145 Layer.provideMerge(
167 Layer.succeed(TextGeneration, { generateBranchName } as unknown as TextGenerationShape),146 Layer.succeed(TextGeneration, { generateBranchName } as unknown as TextGenerationShape),
168 ),147 ),
...@@ -210,11 +189,7 @@ describe("ProviderCommandReactor", () => {...@@ -210,11 +189,7 @@ describe("ProviderCommandReactor", () => {
210 respondToRequest,189 respondToRequest,
211 stopSession,190 stopSession,
212 renameBranch,191 renameBranch,
213 statusDetails,
214 generateBranchName,192 generateBranchName,
215 setStatusBranch: (cwd: string, branch: string | null) => {
216 statusBranchByCwd.set(cwd, branch);
217 },
218 stateDir,193 stateDir,
219 };194 };
220 }195 }
...@@ -260,7 +235,6 @@ describe("ProviderCommandReactor", () => {...@@ -260,7 +235,6 @@ describe("ProviderCommandReactor", () => {
260 it("generates and renames temporary worktree branch on first turn", async () => {235 it("generates and renames temporary worktree branch on first turn", async () => {
261 const harness = await createHarness();236 const harness = await createHarness();
262 const now = new Date().toISOString();237 const now = new Date().toISOString();
263 const worktreeCwd = "/tmp/provider-project/.t3/worktrees/t3code-89abc123";
264238
265 await Effect.runPromise(239 await Effect.runPromise(
266 harness.engine.dispatch({240 harness.engine.dispatch({
...@@ -268,10 +242,9 @@ describe("ProviderCommandReactor", () => {...@@ -268,10 +242,9 @@ describe("ProviderCommandReactor", () => {
268 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch"),242 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch"),
269 threadId: ThreadId.makeUnsafe("thread-1"),243 threadId: ThreadId.makeUnsafe("thread-1"),
270 branch: "t3code/89abc123",244 branch: "t3code/89abc123",
271 worktreePath: worktreeCwd,245 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
272 }),246 }),
273 );247 );
274 harness.setStatusBranch(worktreeCwd, "t3code/89abc123");
275248
276 await Effect.runPromise(249 await Effect.runPromise(
277 harness.engine.dispatch({250 harness.engine.dispatch({
...@@ -302,7 +275,7 @@ describe("ProviderCommandReactor", () => {...@@ -302,7 +275,7 @@ describe("ProviderCommandReactor", () => {
302 await waitFor(() => harness.renameBranch.mock.calls.length === 1);275 await waitFor(() => harness.renameBranch.mock.calls.length === 1);
303276
304 expect(harness.generateBranchName.mock.calls[0]?.[0]).toEqual({277 expect(harness.generateBranchName.mock.calls[0]?.[0]).toEqual({
305 cwd: worktreeCwd,278 cwd: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
306 message: "Fix visual bug from screenshot",279 message: "Fix visual bug from screenshot",
307 attachments: [280 attachments: [
308 {281 {
...@@ -315,14 +288,18 @@ describe("ProviderCommandReactor", () => {...@@ -315,14 +288,18 @@ describe("ProviderCommandReactor", () => {
315 ],288 ],
316 });289 });
317 expect(harness.renameBranch.mock.calls[0]?.[0]).toEqual({290 expect(harness.renameBranch.mock.calls[0]?.[0]).toEqual({
318 cwd: worktreeCwd,291 cwd: "/tmp/provider-project/.t3/worktrees/t3code-89abc123",
319 oldBranch: "t3code/89abc123",292 oldBranch: "t3code/89abc123",
320 newBranch: "t3code/generated-name",293 newBranch: "t3code/generated-name",
321 });294 });
322295
323 const readModel = await Effect.runPromise(harness.engine.getReadModel());296 await waitFor(() => {
324 const thread = readModel.threads.find((entry) => entry.id === ThreadId.makeUnsafe("thread-1"));297 const readModel = Effect.runSync(harness.engine.getReadModel());
325 expect(thread?.branch).toBe("t3code/89abc123");298 const thread = readModel.threads.find(
299 (entry) => entry.id === ThreadId.makeUnsafe("thread-1"),
300 );
301 return thread?.branch === "t3code/generated-name";
302 });
326 });303 });
327304
328 it("passes persisted attachment references to branch generation and turn start", async () => {305 it("passes persisted attachment references to branch generation and turn start", async () => {
...@@ -396,7 +373,6 @@ describe("ProviderCommandReactor", () => {...@@ -396,7 +373,6 @@ describe("ProviderCommandReactor", () => {
396 it("skips worktree branch generation after the first user turn", async () => {373 it("skips worktree branch generation after the first user turn", async () => {
397 const harness = await createHarness();374 const harness = await createHarness();
398 const now = new Date().toISOString();375 const now = new Date().toISOString();
399 const worktreeCwd = "/tmp/provider-project/.t3/worktrees/t3code-1234abcd";
400376
401 await Effect.runPromise(377 await Effect.runPromise(
402 harness.engine.dispatch({378 harness.engine.dispatch({
...@@ -404,10 +380,9 @@ describe("ProviderCommandReactor", () => {...@@ -404,10 +380,9 @@ describe("ProviderCommandReactor", () => {
404 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-2"),380 commandId: CommandId.makeUnsafe("cmd-thread-meta-set-temp-branch-2"),
405 threadId: ThreadId.makeUnsafe("thread-1"),381 threadId: ThreadId.makeUnsafe("thread-1"),
406 branch: "t3code/1234abcd",382 branch: "t3code/1234abcd",
407 worktreePath: worktreeCwd,383 worktreePath: "/tmp/provider-project/.t3/worktrees/t3code-1234abcd",
408 }),384 }),
409 );385 );
410 harness.setStatusBranch(worktreeCwd, "t3code/1234abcd");
411386
412 await Effect.runPromise(387 await Effect.runPromise(
413 harness.engine.dispatch({388 harness.engine.dispatch({
...@@ -450,44 +425,6 @@ describe("ProviderCommandReactor", () => {...@@ -450,44 +425,6 @@ describe("ProviderCommandReactor", () => {
450 expect(harness.renameBranch.mock.calls.length).toBe(1);425 expect(harness.renameBranch.mock.calls.length).toBe(1);
451 });426 });
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
491 it("skips worktree rename when branch-name generation fails", async () => {428 it("skips worktree rename when branch-name generation fails", async () => {
492 const harness = await createHarness();429 const harness = await createHarness();
493 const now = new Date().toISOString();430 const now = new Date().toISOString();
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+11-36
...@@ -330,42 +330,17 @@ const make = Effect.gen(function* () {...@@ -330,42 +330,17 @@ const make = Effect.gen(function* () {
330 const targetBranch = buildGeneratedWorktreeBranchName(generated.branch);330 const targetBranch = buildGeneratedWorktreeBranchName(generated.branch);
331 if (targetBranch === oldBranch) return Effect.void;331 if (targetBranch === oldBranch) return Effect.void;
332332
333 return Effect.gen(function* () {333 return Effect.flatMap(
334 const currentBranch = yield* git.statusDetails(cwd).pipe(334 git.renameBranch({ cwd, oldBranch, newBranch: targetBranch }),
335 Effect.map((status) => status.branch),335 (renamed) =>
336 Effect.catch((error) =>336 orchestrationEngine.dispatch({
337 Effect.logWarning(337 type: "thread.meta.update",
338 "provider command reactor failed to read active worktree branch",338 commandId: serverCommandId("worktree-branch-rename"),
339 {339 threadId: input.threadId,
340 threadId: input.threadId,340 branch: renamed.branch,
341 cwd,341 worktreePath: cwd,
342 oldBranch,342 }),
343 targetBranch,343 );
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 });
369 }),344 }),
370 Effect.catchCause((cause) =>345 Effect.catchCause((cause) =>
371 Effect.logWarning(346 Effect.logWarning(