Compare commits

...
Author SHA1 Message Date
Saoud Rizwan ebdabe65ce chore(sdk): release v0.0.81 2026-08-26 02:21:57 -07:00
Saoud Rizwan 40c3a4dbd8 chore(desktop): release v0.0.19 2026-08-26 02:14:21 -07:00
Saoud Rizwan 6859d00e51 fix(hub): stop shipping full transcripts inside broadcast hub events (#13587)
* fix(hub): stop shipping full transcripts inside broadcast hub events

Every session.updated (and session.created/detached/run.started) event
embedded the session's ENTIRE message transcript via readCoreSessionSnapshot,
even though no consumer reads snapshot.messages off an event — clients fetch
messages with the session.messages command. For a multi-megabyte transcript
this turns every status flip into megabytes per subscriber, floods the
durable event log, and (until the send-queue backpressure fix lands) lets a
slow subscriber balloon the hub process by one full transcript copy per
event — reported as a 25GB cline process on a 16GB Mac.

Strip snapshot.messages centrally in HubServerTransport.publish() so every
current and future event publisher is covered, the event log stores slim
envelopes, and cursor replay stays byte-identical with live fan-out. All
other snapshot fields (status, usage, model, workspace, checkpoint) are kept,
and command replies are untouched.

* fix(hub): never capture the transcript into event/reply snapshots

Replaces the publish-boundary strip with the real fix: don't build
message-bearing snapshots in the first place. emitSessionSnapshot no longer
re-reads the entire transcript from disk on every status flip, and
readCoreSessionSnapshot no longer reads it for any event or reply — a
snapshot is a state notification (status, usage, model, workspace,
checkpoint); the transcript is fetched via the session.messages command.
Checkpoint-restore snapshots (session-versioning-service) are untouched:
restore replies carry messages in their own dedicated field.
2026-08-26 02:07:28 -07:00
Saoud Rizwan 6ba9b9d7b4 chore(cli): release v3.0.59 2026-08-26 01:49:10 -07:00
15 changed files with 125 additions and 23 deletions
+9
View File
@@ -1,5 +1,14 @@
# Cline CLI Changelog
## 3.0.59
- New files are now created with your platform's native line endings
- Fixed the codebase search tool crashing on files that contain a single enormous line
- Cost estimates are no longer shown for Claude Code. Its usage is typically covered by a Claude Pro/Max subscription, but its models reuse Anthropic API pricing, so Cline was showing charges you were not being billed
- Credentials embedded in git remote URLs are now redacted from the workspace information sent to the model
- Installing an MCP server no longer misreads a `--` separator in the install arguments as part of the server command
- Refreshed the model catalog. Adds seven providers (Agnes AI, Aixy, IteraCompute, LLM Tech, NeoSmith, Pendra, and Standard Compute) and updates model lists and pricing across providers. The resolved default model changes for ClinePass (now GLM 5.3), Z.ai, Hugging Face, evroc, LLM Gateway, NanoGPT, and Weights & Biases, so if you use one of those without pinning a model you will get a different default
## 3.0.58
- The first-launch "Try ClinePass" dialog no longer advertises the $4.99 first-month promo, which is ending
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.58",
"version": "3.0.59",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
+5
View File
@@ -1,5 +1,10 @@
# Cline Desktop Changelog
## 0.0.19
- Fixed the background Cline process ballooning in memory during long sessions — session status updates were carrying a full copy of the conversation transcript to every connected client, which on a multi-megabyte task could grow the process to tens of gigabytes. Status updates now carry only state (status, usage, model, workspace, checkpoint); the transcript is fetched on demand
- Refreshed the model catalog. Adds seven providers (Agnes AI, Aixy, IteraCompute, LLM Tech, NeoSmith, Pendra, and Standard Compute) and updates model lists and pricing across providers. The resolved default model changes for ClinePass (now GLM 5.3), Z.ai, Hugging Face, evroc, LLM Gateway, NanoGPT, and Weights & Biases, so if you use one of those without pinning a model you will get a different default
## 0.0.18
- The sidebar is time-sorted again by default, with collapsible Pinned / Scheduled / Tasks sections and a one-click toggle to switch to project grouping (the old dropdown is gone). Scheduled sessions are marked with a clock icon, and the list starts taller and grows to fill the sidebar instead of stranding rows over empty space
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/code",
"version": "0.0.18",
"version": "0.0.19",
"private": true,
"scripts": {
"build:ui": "bun -F @cline/ui build",
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Cline",
"version": "0.0.18",
"version": "0.0.19",
"identifier": "bot.cline.app",
"build": {
"beforeDevCommand": "bun run build:sidecar:bin && bun run dev:web",
+6 -6
View File
@@ -19,7 +19,7 @@
},
"apps/cli": {
"name": "@cline/cli",
"version": "3.0.58",
"version": "3.0.59",
"bin": {
"cline": "src/index.ts",
},
@@ -636,7 +636,7 @@
},
"sdk/packages/agents": {
"name": "@cline/agents",
"version": "0.0.80",
"version": "0.0.81",
"dependencies": {
"@cline/llms": "workspace:*",
"@cline/shared": "workspace:*",
@@ -645,7 +645,7 @@
},
"sdk/packages/core": {
"name": "@cline/core",
"version": "0.0.80",
"version": "0.0.81",
"dependencies": {
"@cline/agents": "workspace:*",
"@cline/llms": "workspace:*",
@@ -683,7 +683,7 @@
},
"sdk/packages/llms": {
"name": "@cline/llms",
"version": "0.0.80",
"version": "0.0.81",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^5.0.50",
"@ai-sdk/anthropic": "^4.0.36",
@@ -730,14 +730,14 @@
},
"sdk/packages/sdk": {
"name": "@cline/sdk",
"version": "0.0.80",
"version": "0.0.81",
"dependencies": {
"@cline/core": "workspace:*",
},
},
"sdk/packages/shared": {
"name": "@cline/shared",
"version": "0.0.80",
"version": "0.0.81",
"dependencies": {
"aws4fetch": "^1.0.20",
"jsonrepair": "^3.13.2",
+4
View File
@@ -1,5 +1,9 @@
# Cline SDK Changelog
## 0.0.81
- Session snapshot events no longer carry the full conversation transcript. Every `session.updated` (and `session.created` / `session.detached` / `run.started`) event embedded the session's entire message history, so on a multi-megabyte task each status flip shipped megabytes to every subscriber, flooded the durable event log, and could grow the hub process by one transcript copy per event — reported as a 25 GB `cline` process on a 16 GB machine. Snapshots are now state-only (status, usage, model, workspace, checkpoint); transcripts are fetched with the `session.messages` command. Checkpoint-restore replies, which carry messages in their own field, are unaffected
## 0.0.80
- New files created by the file-writing tools now use the platform's native line endings
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/agents",
"version": "0.0.80",
"version": "0.0.81",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/core",
"description": "Cline Core SDK for Node Runtime",
"version": "0.0.80",
"version": "0.0.81",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
@@ -462,7 +462,12 @@ describe("HubServerTransport boundaries", () => {
});
expect(snapshotReply.payload).toHaveProperty("snapshot");
expect(readSessionMessages).toHaveBeenCalledWith("session-1");
// Snapshots are state notifications: even when requested they never
// carry (or read) the transcript — that's session.messages' job.
expect(readSessionMessages).not.toHaveBeenCalled();
expect(
snapshotReply.payload?.snapshot as Record<string, unknown>,
).not.toHaveProperty("messages");
});
it("keeps interactive approval requests pending until a response arrives", async () => {
@@ -1238,6 +1243,78 @@ describe("HubServerTransport boundaries", () => {
expect(updateSessionCompactionState).not.toHaveBeenCalled();
});
it("never captures the transcript into event or reply snapshots", async () => {
const transcript = [
{ role: "user", content: [{ type: "text", text: "hello" }] },
{ role: "assistant", content: [{ type: "text", text: "world" }] },
];
let capturedSessionListener:
| ((event: Record<string, unknown>) => void)
| undefined;
const readSessionMessages = vi.fn().mockResolvedValue(transcript);
const transport = createTransport({
sessionHost: {
subscribe: vi.fn(
(listener: (event: Record<string, unknown>) => void) => {
capturedSessionListener = listener;
return () => {};
},
),
updateSession: vi.fn().mockResolvedValue({ updated: true }),
readSessionMessages,
},
});
const ctx = getContext(transport);
const events: HubEventEnvelope[] = [];
ensureSessionState(ctx, "session-1", "owner-client", "creator");
transport.subscribe("owner-client", (event) => events.push(event));
// A status change projects a session.updated whose snapshot carries
// state (status, usage, ...) but never the conversation — the session
// host's transcript must not even be read for it.
capturedSessionListener?.({
type: "status",
payload: { sessionId: "session-1", status: "running" },
});
await vi.waitFor(() => {
expect(events.some((event) => event.event === "session.updated")).toBe(
true,
);
});
const statusEvent = events.find(
(event) => event.event === "session.updated",
);
const statusSnapshot = statusEvent?.payload?.snapshot as Record<
string,
unknown
>;
expect(statusSnapshot.status).toBe("completed");
expect(statusSnapshot).not.toHaveProperty("messages");
// A session.update command: neither the published event nor the reply
// snapshot contains messages (clients fetch them via session.messages).
events.length = 0;
const reply = await transport.handleCommand({
version: "v1",
requestId: "req-update-no-transcript",
command: "session.update",
clientId: "owner-client",
sessionId: "session-1",
payload: { metadata: { title: "renamed" } },
});
const replySnapshot = reply.payload?.snapshot as Record<string, unknown>;
expect(replySnapshot).not.toHaveProperty("messages");
const updated = events.find((event) => event.event === "session.updated");
expect(updated).toBeDefined();
const updatedSnapshot = updated?.payload?.snapshot as Record<
string,
unknown
>;
expect(updatedSnapshot).not.toHaveProperty("messages");
expect(updatedSnapshot.status).toBe("completed");
expect(readSessionMessages).not.toHaveBeenCalled();
});
it("publishes session updates after successful compaction sidecar updates", async () => {
const state = createSessionCompactionState({
sourceMessages: [{ role: "user", content: "source" }],
@@ -185,6 +185,15 @@ export async function readHubSessionRecord(
);
}
/**
* Builds the snapshot that rides on hub events and command replies. It is a
* state notification — status, usage, model, workspace, checkpoint — and
* deliberately does NOT read the transcript: `snapshot.messages` here would
* put the entire conversation on every status flip, for every subscriber,
* and into the durable event log (observed in the wild as a 25GB hub process
* feeding a slow reader). Anything that needs messages fetches them with the
* `session.messages` command.
*/
export async function readCoreSessionSnapshot(
ctx: HubTransportContext,
sessionId: string,
@@ -193,15 +202,9 @@ export async function readCoreSessionSnapshot(
if (!session) {
return undefined;
}
const [messages, usageSummary] = await Promise.all([
typeof ctx.sessionHost.readSessionMessages === "function"
? ctx.sessionHost.readSessionMessages(sessionId)
: [],
ctx.sessionHost.getAccumulatedUsage?.(sessionId),
]);
const usageSummary = await ctx.sessionHost.getAccumulatedUsage?.(sessionId);
return createCoreSessionSnapshot({
session,
messages,
usage: usageSummary?.usage,
aggregateUsage: usageSummary?.aggregateUsage,
});
@@ -2603,6 +2603,11 @@ export class LocalRuntimeHost implements RuntimeHost {
});
}
// The emitted snapshot is a state notification (status, usage, workspace,
// checkpoint) and deliberately omits the transcript: emitStatus fires this
// on every status flip, so including messages would re-read and broadcast
// the entire conversation each time. Consumers that need messages read
// them explicitly via readSessionMessages / the session.messages command.
private async emitSessionSnapshot(sessionId: string): Promise<void> {
const session = await this.getSession(sessionId);
if (!session) return;
@@ -2612,7 +2617,6 @@ export class LocalRuntimeHost implements RuntimeHost {
sessionId,
snapshot: createCoreSessionSnapshot({
session,
messages: await this.readSessionMessages(sessionId),
usage: this.usageBySession.get(sessionId),
aggregateUsage: this.aggregateUsageBySession.get(sessionId),
}),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/llms",
"version": "0.0.80",
"version": "0.0.81",
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/sdk",
"description": "Cline SDK - user-facing alias for @cline/core",
"version": "0.0.80",
"version": "0.0.81",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/shared",
"version": "0.0.80",
"version": "0.0.81",
"description": "Shared utilities, types, and schemas for Cline packages",
"repository": {
"type": "git",