feat: add codex review failover and suppress output hardening

This commit is contained in:
Eyejoker
2026-03-28 05:40:38 +09:00
parent d1c693fb17
commit ba9f6871b6
49 changed files with 3884 additions and 1763 deletions

View File

@@ -870,6 +870,31 @@ describe('DiscordChannel', () => {
expect(currentClient().channels.fetch).not.toHaveBeenCalled();
});
it('does not send stale typing after typing was disabled mid-flight', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);
await channel.connect();
let resolveFetch!: (value: unknown) => void;
const fetchPromise = new Promise((resolve) => {
resolveFetch = resolve;
});
const mockChannel = {
send: vi.fn(),
sendTyping: vi.fn().mockResolvedValue(undefined),
};
currentClient().channels.fetch.mockImplementationOnce(() => fetchPromise);
const typingPromise = channel.setTyping('dc:1234567890123456', true);
await Promise.resolve();
await channel.setTyping('dc:1234567890123456', false);
resolveFetch(mockChannel);
await typingPromise;
expect(mockChannel.sendTyping).not.toHaveBeenCalled();
});
it('does nothing when client is not initialized', async () => {
const opts = createTestOpts();
const channel = new DiscordChannel('test-token', opts);

View File

@@ -177,6 +177,7 @@ export class DiscordChannel implements Channel {
private opts: DiscordChannelOpts;
private botToken: string;
private typingIntervals = new Map<string, NodeJS.Timeout>();
private typingGenerations = new Map<string, number>();
private agentTypeFilter?: AgentType;
constructor(
@@ -539,6 +540,9 @@ export class DiscordChannel implements Channel {
async setTyping(jid: string, isTyping: boolean): Promise<void> {
if (!this.client) return;
const generation = (this.typingGenerations.get(jid) ?? 0) + 1;
this.typingGenerations.set(jid, generation);
// Clear any existing interval for this channel
const existing = this.typingIntervals.get(jid);
if (existing) {
@@ -548,10 +552,15 @@ export class DiscordChannel implements Channel {
if (!isTyping) return;
const isCurrentGeneration = () =>
this.typingGenerations.get(jid) === generation;
const sendOnce = async () => {
if (!isCurrentGeneration()) return;
try {
const channelId = jid.replace(/^dc:/, '');
const channel = await this.client!.channels.fetch(channelId);
if (!isCurrentGeneration()) return;
if (channel && 'sendTyping' in channel) {
await (channel as TextChannel).sendTyping();
}
@@ -562,7 +571,13 @@ export class DiscordChannel implements Channel {
// Send immediately, then refresh every 8 seconds (Discord expires at ~10s)
await sendOnce();
this.typingIntervals.set(jid, setInterval(sendOnce, 8000));
if (!isCurrentGeneration()) return;
this.typingIntervals.set(
jid,
setInterval(() => {
void sendOnce();
}, 8000),
);
}
async sendAndTrack(jid: string, text: string): Promise<string | null> {