Skip to content

Commit 07c5890

Browse files
committed
fix(telemetry): prevent idle timer from breaking tests with fake timers
1 parent a060887 commit 07c5890

5 files changed

Lines changed: 47 additions & 11 deletions

File tree

src/__tests__/history-resume-delegation.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,7 @@ describe("History resume delegation - parent metadata transitions", () => {
12551255
userMessageContent: [],
12561256
consecutiveMistakeCount: 0,
12571257
emitFinalTokenUsageUpdate: vi.fn(),
1258+
flushTelemetryInstallment: vi.fn(),
12581259
} as unknown as import("../core/task/Task").Task
12591260

12601261
const block = {

src/__tests__/nested-delegation-resume.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ describe("Nested delegation resume (A → B → C)", () => {
203203
userMessageContent: [],
204204
consecutiveMistakeCount: 0,
205205
emitFinalTokenUsageUpdate: vi.fn(),
206+
flushTelemetryInstallment: vi.fn(),
206207
} as unknown as Task
207208

208209
const blockC = {
@@ -250,6 +251,7 @@ describe("Nested delegation resume (A → B → C)", () => {
250251
userMessageContent: [],
251252
consecutiveMistakeCount: 0,
252253
emitFinalTokenUsageUpdate: vi.fn(),
254+
flushTelemetryInstallment: vi.fn(),
253255
} as unknown as Task
254256

255257
const blockB = {

src/core/task/Task.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -617,12 +617,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
617617
{ leading: true, trailing: true, maxWait: this.TOKEN_USAGE_EMIT_INTERVAL_MS },
618618
)
619619

620-
this.startIdleTelemetryCheck()
621-
622620
onCreated?.(this)
623621

624622
if (startTask) {
625623
this._started = true
624+
this.startIdleTelemetryCheck()
626625
if (task || images) {
627626
void this.startTask(task, images).catch((error) => {
628627
console.error("[Task#constructor] startTask failed:", error)
@@ -1900,6 +1899,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
19001899
return
19011900
}
19021901
this._started = true
1902+
this.startIdleTelemetryCheck()
19031903

19041904
const { task, images } = this.metadata
19051905

@@ -3697,8 +3697,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
36973697
// userMessageWasRemoved so the message (and its count) is restored
36983698
// exactly once when the retry succeeds, keeping the total symmetric
36993699
// regardless of how many empty-response cycles occur first.
3700+
// Guard: only reverse a count this iteration actually added. The
3701+
// popped message may predate this turn (resumed history, or a
3702+
// message appended by flushPendingToolResultsToHistory, neither
3703+
// of which incremented messageCounts.user).
37003704
this.apiConversationHistory.pop()
3701-
this.messageCounts.user--
3705+
if (shouldAddUserMessage) {
3706+
this.messageCounts.user--
3707+
}
37023708
}
37033709
}
37043710

@@ -4755,8 +4761,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
47554761
}
47564762

47574763
const messageCountDelta = {
4758-
user: this.messageCounts.user - this.telemetryMessageCountsBaseline.user,
4759-
assistant: this.messageCounts.assistant - this.telemetryMessageCountsBaseline.assistant,
4764+
user: Math.max(0, this.messageCounts.user - this.telemetryMessageCountsBaseline.user),
4765+
assistant: Math.max(0, this.messageCounts.assistant - this.telemetryMessageCountsBaseline.assistant),
47604766
}
47614767

47624768
const hasToolUsageDelta = Object.keys(toolUsageDelta).length > 0
@@ -4774,7 +4780,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
47744780
this.lastTelemetryFlushAt = Date.now()
47754781
}
47764782

4777-
private startIdleTelemetryCheck(): void {
4783+
startIdleTelemetryCheck(): void {
47784784
this.idleTelemetryCheckInterval = setInterval(() => {
47794785
// lastMessageTs only moves forward on activity, so comparing it against the
47804786
// last flush tells us whether anything happened since that flush -- if the

src/core/task/__tests__/Task.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3124,12 +3124,14 @@ describe("Telemetry installments (idle/shutdown flush)", () => {
31243124
})
31253125

31263126
function createTask() {
3127-
return new Task({
3127+
const task = new Task({
31283128
provider: mockProvider,
31293129
apiConfiguration: mockApiConfig,
31303130
task: "test task",
31313131
startTask: false,
31323132
})
3133+
task.startIdleTelemetryCheck()
3134+
return task
31333135
}
31343136

31353137
describe("flushTelemetryInstallment", () => {

src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,17 @@ describe("empty-assistant-response retry keeps messageCounts.user symmetric", ()
2525
}
2626
}
2727

28-
function simulatePopOnEmptyResponse(messageCounts: { user: number; assistant: number }) {
29-
// Mirrors Task.ts: popping the just-added user message from apiConversationHistory
30-
// is paired with decrementing messageCounts.user to match.
31-
messageCounts.user--
28+
function simulatePopOnEmptyResponse(
29+
messageCounts: { user: number; assistant: number },
30+
shouldAddUserMessage = true,
31+
) {
32+
// Mirrors Task.ts: pop the last user message from apiConversationHistory and
33+
// decrement messageCounts.user only if this iteration actually incremented it.
34+
// Messages added by flushPendingToolResultsToHistory or loaded from resumed
35+
// history are not counted, so popping them must not decrement.
36+
if (shouldAddUserMessage) {
37+
messageCounts.user--
38+
}
3239
}
3340

3441
function simulateDeclineRetry(messageCounts: { user: number; assistant: number }) {
@@ -103,4 +110,22 @@ describe("empty-assistant-response retry keeps messageCounts.user symmetric", ()
103110
simulateDeclineRetry(messageCounts)
104111
expect(messageCounts).toEqual({ user: 1, assistant: 1 })
105112
})
113+
114+
it("does not go negative when the popped message was not counted by this iteration (e.g. flushPendingToolResultsToHistory)", () => {
115+
// Scenario: a user message was appended to apiConversationHistory by
116+
// flushPendingToolResultsToHistory (or loaded from resumed history) without
117+
// incrementing messageCounts.user. shouldAddUserMessage is false for this
118+
// turn (empty content). The assistant returns nothing and the pop fires.
119+
// The count must stay at 0, not go to -1.
120+
const messageCounts = { user: 0, assistant: 0 }
121+
122+
// This turn: empty content, so shouldAddUserMessageToHistory returns false.
123+
const shouldAddUserMessage = false
124+
simulateAttempt(messageCounts, { retryAttempt: 0, isEmptyUserContent: true, userMessageWasRemoved: false })
125+
expect(messageCounts.user).toBe(0)
126+
127+
// Assistant returns nothing -- pop fires but guard skips the decrement.
128+
simulatePopOnEmptyResponse(messageCounts, shouldAddUserMessage)
129+
expect(messageCounts.user).toBe(0)
130+
})
106131
})

0 commit comments

Comments
 (0)