Skip to content

Commit 5a08be7

Browse files
bra1nDumpclaudehappy-otter
committed
fix: batch outbox flush (latest-first) and log backoff errors
flushOutbox previously sent the entire pendingOutbox in one POST. On --resume with 400+ historical messages, this payload was too large and failed silently (backoff retried forever with no logging), permanently blocking all subsequent agent messages. - Batch flushOutbox into max 50 items per POST, sending from the end first so the user sees the latest messages immediately - Add error logging to global backoff so failures are never silent Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
1 parent 89443af commit 5a08be7

File tree

4 files changed

+31
-24
lines changed

4 files changed

+31
-24
lines changed

packages/happy-cli/src/api/apiSession.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -308,30 +308,32 @@ export class ApiSessionClient extends EventEmitter {
308308
}
309309
}
310310

311-
private async flushOutbox() {
312-
if (this.pendingOutbox.length === 0) {
313-
return;
314-
}
311+
private static readonly MAX_OUTBOX_BATCH_SIZE = 50;
315312

316-
const batch = this.pendingOutbox.slice();
317-
const response = await axios.post<V3PostSessionMessagesResponse>(
318-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
319-
{
320-
messages: batch
321-
},
322-
{
323-
headers: this.authHeaders(),
324-
timeout: 60000
325-
}
326-
);
313+
private async flushOutbox() {
314+
// Send latest messages first so the user sees recent activity immediately,
315+
// then backfill older messages in subsequent batches.
316+
while (this.pendingOutbox.length > 0) {
317+
const batchSize = Math.min(this.pendingOutbox.length, ApiSessionClient.MAX_OUTBOX_BATCH_SIZE);
318+
const batch = this.pendingOutbox.splice(-batchSize, batchSize);
327319

328-
this.pendingOutbox.splice(0, batch.length);
320+
const response = await axios.post<V3PostSessionMessagesResponse>(
321+
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
322+
{
323+
messages: batch
324+
},
325+
{
326+
headers: this.authHeaders(),
327+
timeout: 60000
328+
}
329+
);
329330

330-
const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
331-
const maxSeq = messages.reduce((acc, message) => (
332-
message.seq > acc ? message.seq : acc
333-
), this.lastSeq);
334-
this.lastSeq = maxSeq;
331+
const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
332+
const maxSeq = messages.reduce((acc, message) => (
333+
message.seq > acc ? message.seq : acc
334+
), this.lastSeq);
335+
this.lastSeq = maxSeq;
336+
}
335337
}
336338

337339
private enqueueMessage(content: unknown, invalidate: boolean = true) {

packages/happy-cli/src/claude/utils/sessionScanner.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export async function createSessionScanner(opts: {
4848

4949
// Main sync function
5050
const sync = new InvalidateSync(async () => {
51-
// logger.debug(`[SESSION_SCANNER] Syncing...`);
5251

5352
// Collect session ids - include ALL sessions that have watchers
5453
// This ensures we continue processing sessions that Claude Code may still write to

packages/happy-cli/src/utils/sync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ export class InvalidateSync {
7171
this._notifyPendings();
7272
}
7373
}
74-
}
74+
}

packages/happy-cli/src/utils/time.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { logger } from '@/ui/logger';
2+
13
export async function delay(ms: number) {
24
return new Promise(resolve => setTimeout(resolve, ms));
35
}
@@ -38,4 +40,8 @@ export function createBackoff(
3840
};
3941
}
4042

41-
export let backoff = createBackoff();
43+
export let backoff = createBackoff({
44+
onError: (e, failuresCount) => {
45+
logger.debug(`[BACKOFF] retry ${failuresCount}:`, (e as Error)?.message || e);
46+
}
47+
});

0 commit comments

Comments
 (0)