Skip to content

Commit f6096b1

Browse files
committed
Revert "fix: handle inverse race condition where API user message is before clineMessage"
This reverts commit acc5567.
1 parent acc5567 commit f6096b1

File tree

2 files changed

+18
-127
lines changed

2 files changed

+18
-127
lines changed

src/core/message-manager/index.spec.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -848,61 +848,5 @@ describe("MessageManager", () => {
848848
expect(apiCall[2].ts).toBe(200)
849849
expect(apiCall[2].role).toBe("assistant")
850850
})
851-
852-
it("should handle inverse race condition where user API message is BEFORE clineMessage timestamp", async () => {
853-
// This test covers the edge case where the API user message (tool_result)
854-
// is logged BEFORE the clineMessage timestamp due to inverse timing.
855-
//
856-
// Timeline (inverse race condition scenario):
857-
// - T1 (50): API user (initial request) - Turn 1
858-
// - T2 (100): API assistant (response) - Turn 1
859-
// - T3 (120): API user (tool_result for Turn 2, logged early due to race) - Turn 2, SHOULD DELETE
860-
// - T4 (150): clineMessage "user_feedback" being deleted - Turn 2
861-
// - T5 (180): API assistant (response to Turn 2) - Turn 2, SHOULD DELETE
862-
// - T6 (250): API user (Turn 3) - Turn 3, SHOULD KEEP
863-
//
864-
// When deleting the clineMessage at T4=150, we should:
865-
// - Keep Turn 1 (ts=50, ts=100)
866-
// - Remove Turn 2's API user at ts=120 (even though it's before clineMessage!)
867-
// - Remove Turn 2's assistant at ts=180
868-
// - Keep Turn 3 (ts=250)
869-
//
870-
// The current fix only looks for user messages at or AFTER cutoff,
871-
// so it would incorrectly skip the user at ts=120 and find ts=250 instead.
872-
873-
mockTask.clineMessages = [
874-
{ ts: 50, say: "user", text: "Initial request" },
875-
{ ts: 150, say: "user_feedback", text: "Turn 2 feedback" }, // Being deleted
876-
{ ts: 250, say: "user", text: "Turn 3" },
877-
]
878-
879-
mockTask.apiConversationHistory = [
880-
{ ts: 50, role: "user", content: [{ type: "text", text: "Initial request" }] },
881-
{ ts: 100, role: "assistant", content: [{ type: "text", text: "Response 1" }] },
882-
{
883-
ts: 120, // Inverse race: logged BEFORE clineMessage at ts=150!
884-
role: "user",
885-
content: [{ type: "tool_result", tool_use_id: "tool_1", content: "Turn 2 feedback" }],
886-
},
887-
{
888-
ts: 180, // After clineMessage
889-
role: "assistant",
890-
content: [{ type: "text", text: "Response to Turn 2" }],
891-
},
892-
{ ts: 250, role: "user", content: [{ type: "text", text: "Turn 3" }] },
893-
]
894-
895-
// Delete the user_feedback clineMessage at ts=150
896-
await manager.rewindToTimestamp(150)
897-
898-
// Expected: Keep Turn 1 (ts=50, ts=100), remove Turn 2 (ts=120, ts=180)
899-
// The API user at ts=120 should be removed even though it's before the clineMessage
900-
const apiCall = mockTask.overwriteApiConversationHistory.mock.calls[0][0]
901-
expect(apiCall).toHaveLength(2)
902-
expect(apiCall[0].ts).toBe(50) // Turn 1 user - kept
903-
expect(apiCall[1].ts).toBe(100) // Turn 1 assistant - kept
904-
// ts=120 (Turn 2 user) should be removed
905-
// ts=180 (Turn 2 assistant) should be removed
906-
})
907851
})
908852
})

src/core/message-manager/index.ts

Lines changed: 18 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,11 @@ export class MessageManager {
136136
*
137137
* Note on timestamp handling:
138138
* Due to async execution during streaming, clineMessage timestamps may not
139-
* perfectly align with API message timestamps. There are two race condition scenarios:
140-
*
141-
* 1. Original race: clineMessage timestamp is BEFORE the assistant API message
142-
* (tool execution happens concurrently with stream completion)
143-
* Solution: Find the first API user message at or after the cutoff
144-
*
145-
* 2. Inverse race: API user message (tool_result) timestamp is BEFORE the clineMessage
146-
* (the tool_result was logged before the user_feedback clineMessage)
147-
* Solution: Find the last assistant message before cutoff and use that as boundary
139+
* perfectly align with API message timestamps. Specifically, a "user_feedback"
140+
* clineMessage can have a timestamp BEFORE the assistant API message that
141+
* triggered it (because tool execution happens concurrently with stream
142+
* completion). To handle this race condition, we find the first API user
143+
* message at or after the cutoff and use its timestamp as the actual boundary.
148144
*/
149145
private async truncateApiHistoryWithCleanup(
150146
cutoffTs: number,
@@ -163,35 +159,20 @@ export class MessageManager {
163159
let actualCutoff: number = cutoffTs
164160

165161
if (!hasExactMatch && hasMessageBeforeCutoff) {
166-
// No exact match but there are earlier messages - check for race conditions
167-
//
168-
// First, check for "inverse race" pattern:
169-
// Find the last assistant message before cutoff
170-
const lastAssistantBeforeCutoff = this.findLastAssistantBeforeCutoff(apiHistory, cutoffTs)
171-
172-
if (lastAssistantBeforeCutoff !== undefined) {
173-
// Check if there are user messages between the last assistant and cutoff
174-
// This indicates an "inverse race" where the user's tool_result was logged
175-
// before the clineMessage timestamp
176-
const hasUserBetweenAssistantAndCutoff = apiHistory.some(
177-
(m) =>
178-
m.ts !== undefined && m.ts > lastAssistantBeforeCutoff && m.ts < cutoffTs && m.role === "user",
179-
)
180-
181-
if (hasUserBetweenAssistantAndCutoff) {
182-
// Inverse race detected: use the assistant timestamp + 1 as cutoff
183-
// This ensures we keep the assistant response but remove the user
184-
// message that belongs to the turn being deleted
185-
actualCutoff = lastAssistantBeforeCutoff + 1
186-
} else {
187-
// No inverse race, check for original race condition
188-
// Look for the first API user message at or after the cutoff
189-
actualCutoff = this.findFirstUserCutoff(apiHistory, cutoffTs)
190-
}
191-
} else {
192-
// No assistant before cutoff, use original race condition logic
193-
actualCutoff = this.findFirstUserCutoff(apiHistory, cutoffTs)
162+
// No exact match but there are earlier messages means we might have a race
163+
// condition where the clineMessage timestamp is earlier than any API message
164+
// due to async execution. In this case, look for the first API user message
165+
// at or after the cutoff to use as the actual boundary.
166+
// This ensures assistant messages that preceded the user's response are preserved.
167+
const firstUserMsgIndexToRemove = apiHistory.findIndex(
168+
(m) => m.ts !== undefined && m.ts >= cutoffTs && m.role === "user",
169+
)
170+
171+
if (firstUserMsgIndexToRemove !== -1) {
172+
// Use the user message's timestamp as the actual cutoff
173+
actualCutoff = apiHistory[firstUserMsgIndexToRemove].ts!
194174
}
175+
// else: no user message found, use original cutoffTs (fallback)
195176
}
196177

197178
// Step 2: Filter by the actual cutoff timestamp
@@ -234,38 +215,4 @@ export class MessageManager {
234215
await this.task.overwriteApiConversationHistory(apiHistory)
235216
}
236217
}
237-
238-
/**
239-
* Find the timestamp of the last assistant message before the cutoff.
240-
* Returns undefined if no assistant message exists before cutoff.
241-
*/
242-
private findLastAssistantBeforeCutoff(apiHistory: ApiMessage[], cutoffTs: number): number | undefined {
243-
let lastAssistantTs: number | undefined
244-
245-
for (const msg of apiHistory) {
246-
if (msg.ts !== undefined && msg.ts < cutoffTs && msg.role === "assistant") {
247-
if (lastAssistantTs === undefined || msg.ts > lastAssistantTs) {
248-
lastAssistantTs = msg.ts
249-
}
250-
}
251-
}
252-
253-
return lastAssistantTs
254-
}
255-
256-
/**
257-
* Find the cutoff based on the first user message at or after the original cutoff.
258-
* Falls back to the original cutoff if no user message is found.
259-
*/
260-
private findFirstUserCutoff(apiHistory: ApiMessage[], cutoffTs: number): number {
261-
const firstUserMsgIndex = apiHistory.findIndex(
262-
(m) => m.ts !== undefined && m.ts >= cutoffTs && m.role === "user",
263-
)
264-
265-
if (firstUserMsgIndex !== -1) {
266-
return apiHistory[firstUserMsgIndex].ts!
267-
}
268-
269-
return cutoffTs
270-
}
271218
}

0 commit comments

Comments
 (0)