Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit ada520c

Browse files
committed
Merge main
2 parents e52a743 + 2584504 commit ada520c

46 files changed

Lines changed: 1202 additions & 251 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Roo Code Changelog
22

3+
## [3.44.0] - 2026-01-26
4+
5+
![3.44.0 Release - Worktrees](/releases/3.44.0-release.png)
6+
7+
- Add worktree selector and creation UX (PR #10940 by @brunobergher, thanks Cline!)
8+
- Improve subtask visibility and navigation in history and chat views (PR #10864 by @brunobergher)
9+
- Add wildcard support for MCP alwaysAllow configuration (PR #10948 by @app/roomote)
10+
- Fix: Prevent nested condensing from including previously-condensed content (PR #10985 by @hannesrudolph)
11+
- Fix: VS Code LM token counting returns 0 outside requests, breaking context condensing (#10968 by @srulyt, PR #10983 by @daniel-lxs)
12+
- Fix: Record truncation event when condensation fails but truncation succeeds (PR #10984 by @hannesrudolph)
13+
- Replace hyphen encoding with fuzzy matching for MCP tool names (PR #10775 by @daniel-lxs)
14+
- Remove MCP SERVERS section from system prompt for cleaner prompts (PR #10895 by @daniel-lxs)
15+
- new_task tool creates checkpoint the same way write_to_file does (PR #10982 by @daniel-lxs)
16+
- Update Fireworks provider with new models (#10674 by @hannesrudolph, PR #10679 by @ThanhNguyxn)
17+
- Fix: Truncate AWS Bedrock toolUseId to 64 characters (PR #10902 by @daniel-lxs)
18+
- Fix: Restore opaque background to settings section headers (PR #10951 by @app/roomote)
19+
- Fix: Remove unsupported Fireworks model tool fields (PR #10937 by @app/roomote)
20+
- Update and improve zh-TW Traditional Chinese locale and docs (PR #10953 by @PeterDaveHello)
21+
- Chore: Remove POWER_STEERING experiment remnants (PR #10980 by @hannesrudolph)
22+
323
## [3.43.0] - 2026-01-23
424

525
![3.43.0 Release - Intelligent Context Condensation](/releases/3.43.0-release.png)

packages/types/src/experiment.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
77
*/
88

99
export const experimentIds = [
10-
"powerSteering",
1110
"preventFocusDisruption",
1211
"imageGeneration",
1312
"runSlashCommand",
@@ -24,7 +23,6 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
2423
*/
2524

2625
export const experimentsSchema = z.object({
27-
powerSteering: z.boolean().optional(),
2826
preventFocusDisruption: z.boolean().optional(),
2927
imageGeneration: z.boolean().optional(),
3028
runSlashCommand: z.boolean().optional(),

releases/3.44.0-release.png

1.93 MB
Loading

src/api/providers/__tests__/lite-llm.spec.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,4 +718,206 @@ describe("LiteLLMHandler", () => {
718718
})
719719
})
720720
})
721+
722+
describe("tool ID normalization", () => {
723+
it("should truncate tool IDs longer than 64 characters", async () => {
724+
const optionsWithBedrock: ApiHandlerOptions = {
725+
...mockOptions,
726+
litellmModelId: "bedrock/anthropic.claude-3-sonnet",
727+
}
728+
handler = new LiteLLMHandler(optionsWithBedrock)
729+
730+
vi.spyOn(handler as any, "fetchModel").mockResolvedValue({
731+
id: "bedrock/anthropic.claude-3-sonnet",
732+
info: { ...litellmDefaultModelInfo, maxTokens: 8192 },
733+
})
734+
735+
// Create a tool ID longer than 64 characters
736+
const longToolId = "toolu_" + "a".repeat(70) // 76 characters total
737+
738+
const systemPrompt = "You are a helpful assistant"
739+
const messages: Anthropic.Messages.MessageParam[] = [
740+
{ role: "user", content: "Hello" },
741+
{
742+
role: "assistant",
743+
content: [
744+
{ type: "text", text: "I'll help you with that." },
745+
{ type: "tool_use", id: longToolId, name: "read_file", input: { path: "test.txt" } },
746+
],
747+
},
748+
{
749+
role: "user",
750+
content: [{ type: "tool_result", tool_use_id: longToolId, content: "file contents" }],
751+
},
752+
]
753+
754+
const mockStream = {
755+
async *[Symbol.asyncIterator]() {
756+
yield {
757+
choices: [{ delta: { content: "Response" } }],
758+
usage: { prompt_tokens: 100, completion_tokens: 20 },
759+
}
760+
},
761+
}
762+
763+
mockCreate.mockReturnValue({
764+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
765+
})
766+
767+
const generator = handler.createMessage(systemPrompt, messages)
768+
for await (const _chunk of generator) {
769+
// Consume
770+
}
771+
772+
// Verify that tool IDs are truncated to 64 characters or less
773+
const createCall = mockCreate.mock.calls[0][0]
774+
const assistantMessage = createCall.messages.find(
775+
(msg: any) => msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0,
776+
)
777+
const toolMessage = createCall.messages.find((msg: any) => msg.role === "tool")
778+
779+
expect(assistantMessage).toBeDefined()
780+
expect(assistantMessage.tool_calls[0].id.length).toBeLessThanOrEqual(64)
781+
782+
expect(toolMessage).toBeDefined()
783+
expect(toolMessage.tool_call_id.length).toBeLessThanOrEqual(64)
784+
})
785+
786+
it("should not modify tool IDs that are already within 64 characters", async () => {
787+
const optionsWithBedrock: ApiHandlerOptions = {
788+
...mockOptions,
789+
litellmModelId: "bedrock/anthropic.claude-3-sonnet",
790+
}
791+
handler = new LiteLLMHandler(optionsWithBedrock)
792+
793+
vi.spyOn(handler as any, "fetchModel").mockResolvedValue({
794+
id: "bedrock/anthropic.claude-3-sonnet",
795+
info: { ...litellmDefaultModelInfo, maxTokens: 8192 },
796+
})
797+
798+
// Create a tool ID within 64 characters
799+
const shortToolId = "toolu_01ABC123" // Well under 64 characters
800+
801+
const systemPrompt = "You are a helpful assistant"
802+
const messages: Anthropic.Messages.MessageParam[] = [
803+
{ role: "user", content: "Hello" },
804+
{
805+
role: "assistant",
806+
content: [
807+
{ type: "text", text: "I'll help you with that." },
808+
{ type: "tool_use", id: shortToolId, name: "read_file", input: { path: "test.txt" } },
809+
],
810+
},
811+
{
812+
role: "user",
813+
content: [{ type: "tool_result", tool_use_id: shortToolId, content: "file contents" }],
814+
},
815+
]
816+
817+
const mockStream = {
818+
async *[Symbol.asyncIterator]() {
819+
yield {
820+
choices: [{ delta: { content: "Response" } }],
821+
usage: { prompt_tokens: 100, completion_tokens: 20 },
822+
}
823+
},
824+
}
825+
826+
mockCreate.mockReturnValue({
827+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
828+
})
829+
830+
const generator = handler.createMessage(systemPrompt, messages)
831+
for await (const _chunk of generator) {
832+
// Consume
833+
}
834+
835+
// Verify that tool IDs are unchanged
836+
const createCall = mockCreate.mock.calls[0][0]
837+
const assistantMessage = createCall.messages.find(
838+
(msg: any) => msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0,
839+
)
840+
const toolMessage = createCall.messages.find((msg: any) => msg.role === "tool")
841+
842+
expect(assistantMessage).toBeDefined()
843+
expect(assistantMessage.tool_calls[0].id).toBe(shortToolId)
844+
845+
expect(toolMessage).toBeDefined()
846+
expect(toolMessage.tool_call_id).toBe(shortToolId)
847+
})
848+
849+
it("should maintain uniqueness with hash suffix when truncating", async () => {
850+
const optionsWithBedrock: ApiHandlerOptions = {
851+
...mockOptions,
852+
litellmModelId: "bedrock/anthropic.claude-3-sonnet",
853+
}
854+
handler = new LiteLLMHandler(optionsWithBedrock)
855+
856+
vi.spyOn(handler as any, "fetchModel").mockResolvedValue({
857+
id: "bedrock/anthropic.claude-3-sonnet",
858+
info: { ...litellmDefaultModelInfo, maxTokens: 8192 },
859+
})
860+
861+
// Create two tool IDs that differ only near the end
862+
const longToolId1 = "toolu_" + "a".repeat(60) + "_suffix1"
863+
const longToolId2 = "toolu_" + "a".repeat(60) + "_suffix2"
864+
865+
const systemPrompt = "You are a helpful assistant"
866+
const messages: Anthropic.Messages.MessageParam[] = [
867+
{ role: "user", content: "Hello" },
868+
{
869+
role: "assistant",
870+
content: [
871+
{ type: "text", text: "I'll help." },
872+
{ type: "tool_use", id: longToolId1, name: "read_file", input: { path: "test1.txt" } },
873+
{ type: "tool_use", id: longToolId2, name: "read_file", input: { path: "test2.txt" } },
874+
],
875+
},
876+
{
877+
role: "user",
878+
content: [
879+
{ type: "tool_result", tool_use_id: longToolId1, content: "file1 contents" },
880+
{ type: "tool_result", tool_use_id: longToolId2, content: "file2 contents" },
881+
],
882+
},
883+
]
884+
885+
const mockStream = {
886+
async *[Symbol.asyncIterator]() {
887+
yield {
888+
choices: [{ delta: { content: "Response" } }],
889+
usage: { prompt_tokens: 100, completion_tokens: 20 },
890+
}
891+
},
892+
}
893+
894+
mockCreate.mockReturnValue({
895+
withResponse: vi.fn().mockResolvedValue({ data: mockStream }),
896+
})
897+
898+
const generator = handler.createMessage(systemPrompt, messages)
899+
for await (const _chunk of generator) {
900+
// Consume
901+
}
902+
903+
// Verify that truncated tool IDs are unique (hash suffix ensures this)
904+
const createCall = mockCreate.mock.calls[0][0]
905+
const assistantMessage = createCall.messages.find(
906+
(msg: any) => msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0,
907+
)
908+
909+
expect(assistantMessage).toBeDefined()
910+
expect(assistantMessage.tool_calls).toHaveLength(2)
911+
912+
const id1 = assistantMessage.tool_calls[0].id
913+
const id2 = assistantMessage.tool_calls[1].id
914+
915+
// Both should be truncated to 64 characters
916+
expect(id1.length).toBeLessThanOrEqual(64)
917+
expect(id2.length).toBeLessThanOrEqual(64)
918+
919+
// They should be different (hash suffix ensures uniqueness)
920+
expect(id1).not.toBe(id2)
921+
})
922+
})
721923
})

src/api/providers/__tests__/vscode-lm.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,66 @@ describe("VsCodeLmHandler", () => {
437437
})
438438
})
439439

440+
describe("countTokens", () => {
441+
beforeEach(() => {
442+
handler["client"] = mockLanguageModelChat
443+
})
444+
445+
it("should count tokens when called outside of an active request", async () => {
446+
// Ensure no active request cancellation token exists
447+
handler["currentRequestCancellation"] = null
448+
449+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(42)
450+
451+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello world" }]
452+
const result = await handler.countTokens(content)
453+
454+
expect(result).toBe(42)
455+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Hello world", expect.any(Object))
456+
})
457+
458+
it("should count tokens when called during an active request", async () => {
459+
// Simulate an active request with a cancellation token
460+
const mockCancellation = {
461+
token: { isCancellationRequested: false, onCancellationRequested: vi.fn() },
462+
cancel: vi.fn(),
463+
dispose: vi.fn(),
464+
}
465+
handler["currentRequestCancellation"] = mockCancellation as any
466+
467+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(50)
468+
469+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Test content" }]
470+
const result = await handler.countTokens(content)
471+
472+
expect(result).toBe(50)
473+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("Test content", mockCancellation.token)
474+
})
475+
476+
it("should return 0 when no client is available", async () => {
477+
handler["client"] = null
478+
handler["currentRequestCancellation"] = null
479+
480+
const content: Anthropic.Messages.ContentBlockParam[] = [{ type: "text", text: "Hello" }]
481+
const result = await handler.countTokens(content)
482+
483+
expect(result).toBe(0)
484+
})
485+
486+
it("should handle image blocks with placeholder", async () => {
487+
handler["currentRequestCancellation"] = null
488+
mockLanguageModelChat.countTokens.mockResolvedValueOnce(5)
489+
490+
const content: Anthropic.Messages.ContentBlockParam[] = [
491+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } },
492+
]
493+
const result = await handler.countTokens(content)
494+
495+
expect(result).toBe(5)
496+
expect(mockLanguageModelChat.countTokens).toHaveBeenCalledWith("[IMAGE]", expect.any(Object))
497+
})
498+
})
499+
440500
describe("completePrompt", () => {
441501
it("should complete single prompt", async () => {
442502
const mockModel = { ...mockLanguageModelChat }

src/api/providers/lite-llm.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ApiHandlerOptions } from "../../shared/api"
99

1010
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
1111
import { convertToOpenAiMessages } from "../transform/openai-format"
12+
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
1213

1314
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1415
import { RouterProvider } from "./router-provider"
@@ -115,7 +116,9 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa
115116
): ApiStream {
116117
const { id: modelId, info } = await this.fetchModel()
117118

118-
const openAiMessages = convertToOpenAiMessages(messages)
119+
const openAiMessages = convertToOpenAiMessages(messages, {
120+
normalizeToolCallId: sanitizeOpenAiCallId,
121+
})
119122

120123
// Prepare messages with cache control if enabled and supported
121124
let systemMessage: OpenAI.Chat.ChatCompletionMessageParam

src/api/providers/vscode-lm.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,31 +229,37 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
229229
return 0
230230
}
231231

232-
if (!this.currentRequestCancellation) {
233-
console.warn("Roo Code <Language Model API>: No cancellation token available for token counting")
234-
return 0
235-
}
236-
237232
// Validate input
238233
if (!text) {
239234
console.debug("Roo Code <Language Model API>: Empty text provided for token counting")
240235
return 0
241236
}
242237

238+
// Create a temporary cancellation token if we don't have one (e.g., when called outside a request)
239+
let cancellationToken: vscode.CancellationToken
240+
let tempCancellation: vscode.CancellationTokenSource | null = null
241+
242+
if (this.currentRequestCancellation) {
243+
cancellationToken = this.currentRequestCancellation.token
244+
} else {
245+
tempCancellation = new vscode.CancellationTokenSource()
246+
cancellationToken = tempCancellation.token
247+
}
248+
243249
try {
244250
// Handle different input types
245251
let tokenCount: number
246252

247253
if (typeof text === "string") {
248-
tokenCount = await this.client.countTokens(text, this.currentRequestCancellation.token)
254+
tokenCount = await this.client.countTokens(text, cancellationToken)
249255
} else if (text instanceof vscode.LanguageModelChatMessage) {
250256
// For chat messages, ensure we have content
251257
if (!text.content || (Array.isArray(text.content) && text.content.length === 0)) {
252258
console.debug("Roo Code <Language Model API>: Empty chat message content")
253259
return 0
254260
}
255261
const countMessage = extractTextCountFromMessage(text)
256-
tokenCount = await this.client.countTokens(countMessage, this.currentRequestCancellation.token)
262+
tokenCount = await this.client.countTokens(countMessage, cancellationToken)
257263
} else {
258264
console.warn("Roo Code <Language Model API>: Invalid input type for token counting")
259265
return 0
@@ -287,6 +293,11 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
287293
}
288294

289295
return 0 // Fallback to prevent stream interruption
296+
} finally {
297+
// Clean up temporary cancellation token
298+
if (tempCancellation) {
299+
tempCancellation.dispose()
300+
}
290301
}
291302
}
292303

0 commit comments

Comments
 (0)