Skip to content

Commit 0ac3d9f

Browse files
omegent-app[bot]patroza
authored andcommitted
fix(discord-bot): stop double channel rename for mid-busy ▫️ (#287)
Busy was painted first, then VCS "no PR" re-renamed ⏳ Title → ▫️ ⏳ Title. Defer the no-PR badge while still busy so settle composes ▫️ and clears ⏳ in one rename. Real PR upgrades still apply mid-turn. Co-authored-by: omegent-app[bot] <306514130+omegent-app[bot]@users.noreply.github.com> Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
1 parent 60df778 commit 0ac3d9f

2 files changed

Lines changed: 213 additions & 3 deletions

File tree

apps/discord-bot/src/features/ResponseBridge.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ import {
6161
resolveDiscordThreadTitleBadges,
6262
resolveDiscordTitlePrEvidence,
6363
resolveSettledDiscordThreadTitleUpgrade,
64+
resolveSilentDiscordThreadPrBadge,
6465
resolveTemporaryDiscordThreadTitleBadge,
6566
resolveThreadChangeRequestLookupCwds,
6667
mergeStickyTitlePr,
6768
nextMirroredThreadTitleAfterApply,
6869
planDiscordThreadTitleApply,
6970
shouldApplyDiscordThreadPrBadge,
7071
shouldApplyDiscordThreadTitleBadge,
72+
shouldDeferInitializedPrWhileBusy,
7173
shouldConvertWorkingTipsToWakeUp,
7274
summarizeExternalUserInput,
7375
threadTitleChangeRequestState,
@@ -2095,6 +2097,143 @@ describe("resolveSettledDiscordThreadTitleUpgrade", () => {
20952097
}),
20962098
).toBe("🔀 ⏳ Empasa pickup carrier rollout");
20972099
});
2100+
2101+
it("does not thrash-rename ⏳ → ▫️ ⏳ while still busy after no-PR is confirmed", () => {
2102+
// Turn start painted activity-only; VCS later confirms no PR. Second rename to
2103+
// ▫️ ⏳ is pure channel noise — settle will paint ▫️ when busy clears.
2104+
expect(
2105+
resolveSettledDiscordThreadTitleUpgrade({
2106+
thread: {
2107+
title: "for pr 2101, i wish we could push freely to draft PRs",
2108+
branch: "t3-discord/pr-2101-draft-pushes",
2109+
worktreePath: "/var/lib/t3/worktrees/t3code/t3-discord-6c17cdca",
2110+
messages: [assistantMessage()],
2111+
session: { status: "running", activeTurnId: "turn-1" } as never,
2112+
latestTurn: {
2113+
turnId: "turn-1" as never,
2114+
state: "running",
2115+
completedAt: null,
2116+
} as never,
2117+
},
2118+
mirroredThreadTitle: "⏳ for pr 2101, i wish we could push freely to draft PRs",
2119+
attemptedThreadTitle: "⏳ for pr 2101, i wish we could push freely to draft PRs",
2120+
cachedPr: null,
2121+
canApplyNoPrBadge: true,
2122+
}),
2123+
).toBeNull();
2124+
});
2125+
2126+
it("still paints ▫️ ⏳ in one shot when busy starts on a plain title with no-PR known", () => {
2127+
expect(
2128+
resolveSettledDiscordThreadTitleUpgrade({
2129+
thread: {
2130+
title: "for pr 2101, i wish we could push freely to draft PRs",
2131+
branch: "t3-discord/pr-2101-draft-pushes",
2132+
worktreePath: "/var/lib/t3/worktrees/t3code/t3-discord-6c17cdca",
2133+
messages: [assistantMessage()],
2134+
session: { status: "running", activeTurnId: "turn-1" } as never,
2135+
latestTurn: {
2136+
turnId: "turn-1" as never,
2137+
state: "running",
2138+
completedAt: null,
2139+
} as never,
2140+
},
2141+
mirroredThreadTitle: "for pr 2101, i wish we could push freely to draft PRs",
2142+
attemptedThreadTitle: "for pr 2101, i wish we could push freely to draft PRs",
2143+
cachedPr: null,
2144+
canApplyNoPrBadge: true,
2145+
}),
2146+
).toBe("▫️ ⏳ for pr 2101, i wish we could push freely to draft PRs");
2147+
});
2148+
2149+
it("still upgrades ⏳ → 🔀 ⏳ mid-turn when a real PR appears", () => {
2150+
expect(
2151+
resolveSettledDiscordThreadTitleUpgrade({
2152+
thread: {
2153+
title: "Empasa pickup carrier rollout",
2154+
branch: "t3-discord/empasa-pickup-carrier",
2155+
worktreePath: "/var/lib/t3/worktrees/scanner/t3-discord-c434b753",
2156+
messages: [assistantMessage()],
2157+
session: { status: "running", activeTurnId: "turn-1" } as never,
2158+
latestTurn: {
2159+
turnId: "turn-1" as never,
2160+
state: "running",
2161+
completedAt: null,
2162+
} as never,
2163+
},
2164+
mirroredThreadTitle: "⏳ Empasa pickup carrier rollout",
2165+
attemptedThreadTitle: "⏳ Empasa pickup carrier rollout",
2166+
cachedPr: { state: "open", hasFailingChecks: false },
2167+
}),
2168+
).toBe("🔀 ⏳ Empasa pickup carrier rollout");
2169+
});
2170+
});
2171+
2172+
describe("shouldDeferInitializedPrWhileBusy / resolveSilentDiscordThreadPrBadge", () => {
2173+
it("defers only the no-PR badge while busy is unchanged", () => {
2174+
expect(
2175+
shouldDeferInitializedPrWhileBusy({
2176+
currentPr: null,
2177+
nextPr: "initialized",
2178+
currentActivity: "busy",
2179+
nextActivity: "busy",
2180+
}),
2181+
).toBe(true);
2182+
expect(
2183+
resolveSilentDiscordThreadPrBadge({
2184+
currentPr: null,
2185+
nextPr: "initialized",
2186+
currentActivity: "busy",
2187+
nextActivity: "busy",
2188+
}),
2189+
).toBeNull();
2190+
});
2191+
2192+
it("does not defer when painting dual badges from a plain title", () => {
2193+
expect(
2194+
shouldDeferInitializedPrWhileBusy({
2195+
currentPr: null,
2196+
nextPr: "initialized",
2197+
currentActivity: null,
2198+
nextActivity: "busy",
2199+
}),
2200+
).toBe(false);
2201+
expect(
2202+
resolveSilentDiscordThreadPrBadge({
2203+
currentPr: null,
2204+
nextPr: "initialized",
2205+
currentActivity: null,
2206+
nextActivity: "busy",
2207+
}),
2208+
).toBe("initialized");
2209+
});
2210+
2211+
it("does not defer real PR upgrades or settle (busy → idle)", () => {
2212+
expect(
2213+
shouldDeferInitializedPrWhileBusy({
2214+
currentPr: null,
2215+
nextPr: "open",
2216+
currentActivity: "busy",
2217+
nextActivity: "busy",
2218+
}),
2219+
).toBe(false);
2220+
expect(
2221+
shouldDeferInitializedPrWhileBusy({
2222+
currentPr: null,
2223+
nextPr: "initialized",
2224+
currentActivity: "busy",
2225+
nextActivity: null,
2226+
}),
2227+
).toBe(false);
2228+
expect(
2229+
resolveSilentDiscordThreadPrBadge({
2230+
currentPr: null,
2231+
nextPr: "initialized",
2232+
currentActivity: "busy",
2233+
nextActivity: null,
2234+
}),
2235+
).toBe("initialized");
2236+
});
20982237
});
20992238

21002239
describe("planDiscordThreadTitleApply", () => {

apps/discord-bot/src/features/ResponseBridge.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,53 @@ export function shouldApplyDiscordThreadPrBadge(
15531553
return discordThreadTitleBadgeRank(next) >= discordThreadTitleBadgeRank(current);
15541554
}
15551555

1556+
/**
1557+
* Silence: never thrash-rename `⏳ Title` → `▫️ ⏳ Title` mid-turn.
1558+
*
1559+
* Busy is painted first (before VCS confirms no PR). Adding only the no-PR ▫️
1560+
* badge while still busy is a second channel-name system message with no user
1561+
* value — settle can compose ▫️ and clear ⏳ in one rename.
1562+
*
1563+
* Real PR upgrades (🔀 / ❌ / ✔️ / ✖️) still apply mid-turn.
1564+
*/
1565+
export function shouldDeferInitializedPrWhileBusy(input: {
1566+
readonly currentPr: DiscordThreadPrBadgeState;
1567+
readonly nextPr: DiscordThreadPrBadgeState;
1568+
readonly currentActivity: DiscordThreadActivityBadgeState;
1569+
readonly nextActivity: DiscordThreadActivityBadgeState;
1570+
}): boolean {
1571+
return (
1572+
input.nextPr === "initialized" &&
1573+
input.currentPr === null &&
1574+
input.currentActivity === "busy" &&
1575+
input.nextActivity === "busy"
1576+
);
1577+
}
1578+
1579+
/**
1580+
* PR column after demotion guards + mid-busy ▫️ silence.
1581+
*/
1582+
export function resolveSilentDiscordThreadPrBadge(input: {
1583+
readonly currentPr: DiscordThreadPrBadgeState;
1584+
readonly nextPr: DiscordThreadPrBadgeState;
1585+
readonly currentActivity: DiscordThreadActivityBadgeState;
1586+
readonly nextActivity: DiscordThreadActivityBadgeState;
1587+
}): DiscordThreadPrBadgeState {
1588+
const prAllowed = shouldApplyDiscordThreadPrBadge(input.currentPr, input.nextPr);
1589+
const appliedPr = prAllowed ? input.nextPr : input.currentPr;
1590+
if (
1591+
shouldDeferInitializedPrWhileBusy({
1592+
currentPr: input.currentPr,
1593+
nextPr: appliedPr,
1594+
currentActivity: input.currentActivity,
1595+
nextActivity: input.nextActivity,
1596+
})
1597+
) {
1598+
return input.currentPr;
1599+
}
1600+
return appliedPr;
1601+
}
1602+
15561603
/**
15571604
* @deprecated Dual-slot: use `shouldApplyDiscordThreadPrBadge` for PR; activity always applies.
15581605
* Kept for older tests that still pass exclusive states.
@@ -1618,12 +1665,19 @@ export function resolveSettledDiscordThreadTitleUpgrade(input: {
16181665
: parseDiscordThreadTitleBadges(input.attemptedThreadTitle);
16191666

16201667
// Sticky PR: refuse demotion; keep current PR column when next would weaken it.
1668+
// Also silence mid-busy ▫️-only upgrades (⏳ Title → ▫️ ⏳ Title thrash).
16211669
const prAllowed = shouldApplyDiscordThreadPrBadge(currentBadges.pr, prState);
1622-
const appliedPr = prAllowed ? prState : currentBadges.pr;
1670+
const appliedPr = resolveSilentDiscordThreadPrBadge({
1671+
currentPr: currentBadges.pr,
1672+
nextPr: prState,
1673+
currentActivity: currentBadges.activity,
1674+
nextActivity: activity,
1675+
});
16231676

16241677
// Demotion refused and activity unchanged → leave the mirrored title alone
16251678
// (preserves ❌ 🔀 etc. without re-decorating from a null PR cache).
1626-
if (!prAllowed && activity === currentBadges.activity) {
1679+
// Same for deferred ▫️ while still busy: nothing to rename until settle.
1680+
if (appliedPr === currentBadges.pr && activity === currentBadges.activity) {
16271681
return null;
16281682
}
16291683

@@ -4370,7 +4424,14 @@ export const runBridge = (
43704424
? mirroredBadges
43714425
: parseDiscordThreadTitleBadges(latest.attemptedThreadTitle);
43724426
const prAllowed = shouldApplyDiscordThreadPrBadge(currentBadges.pr, effectivePrState);
4373-
const appliedPr = prAllowed ? effectivePrState : currentBadges.pr;
4427+
// Silence mid-busy ▫️-only upgrades so VCS "no PR" does not second-rename
4428+
// after the busy paint (⏳ → ▫️ ⏳ thrash). Settle composes both later.
4429+
const appliedPr = resolveSilentDiscordThreadPrBadge({
4430+
currentPr: currentBadges.pr,
4431+
nextPr: effectivePrState,
4432+
currentActivity: currentBadges.activity,
4433+
nextActivity: activityAfterLookup,
4434+
});
43744435

43754436
// Still nothing actionable (no PR column, no activity, nothing mirrored).
43764437
if (appliedPr === null && activityAfterLookup === null && currentBadges.pr === null) {
@@ -4386,6 +4447,16 @@ export const runBridge = (
43864447
return;
43874448
}
43884449

4450+
// Deferred ▫️ while still busy and activity already matches → no rename.
4451+
if (
4452+
appliedPr === currentBadges.pr &&
4453+
activityAfterLookup === currentBadges.activity &&
4454+
latest.mirroredThreadTitle !== null
4455+
) {
4456+
yield* commitComputedTitle(null, "silence-defer-initialized-while-busy");
4457+
return;
4458+
}
4459+
43894460
const desiredTitle = decorateDiscordThreadTitle(
43904461
threadAfterLookup.title,
43914462
{

0 commit comments

Comments
 (0)