Compare commits

...
Author SHA1 Message Date
Mohamed Boudra 5afc8b43f4 chore(release): cut 0.1.110 2026-07-16 22:28:58 +02:00
Mohamed Boudra d13b94e943 docs: add 0.1.110 changelog 2026-07-16 22:25:24 +02:00
Mohamed Boudra 42cf3e12dd fix(acp): keep foreground agents running (#2148)
ACP setup and out-of-prompt notifications do not define a turn lifecycle. Treating them as autonomous turns could complete the active run while its prompt was still streaming.
2026-07-16 20:49:13 +02:00
15 changed files with 84 additions and 146 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog # Changelog
## 0.1.110 - 2026-07-16
### Fixed
- Kimi and other ACP agents now stay marked as running while a response is actively streaming ([#2148](https://github.com/getpaseo/paseo/pull/2148))
## 0.1.109 - 2026-07-16 ## 0.1.109 - 2026-07-16
### Fixed ### Fixed
+21 -21
View File
@@ -1,12 +1,12 @@
{ {
"name": "paseo", "name": "paseo",
"version": "0.1.109", "version": "0.1.110",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "paseo", "name": "paseo",
"version": "0.1.109", "version": "0.1.110",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"workspaces": [ "workspaces": [
@@ -35162,7 +35162,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@getpaseo/app", "name": "@getpaseo/app",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@@ -36181,12 +36181,12 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@getpaseo/cli", "name": "@getpaseo/cli",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"@clack/prompts": "^1.0.0", "@clack/prompts": "^1.0.0",
"@getpaseo/client": "0.1.109", "@getpaseo/client": "0.1.110",
"@getpaseo/protocol": "0.1.109", "@getpaseo/protocol": "0.1.110",
"@getpaseo/server": "0.1.109", "@getpaseo/server": "0.1.110",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
@@ -36432,10 +36432,10 @@
}, },
"packages/client": { "packages/client": {
"name": "@getpaseo/client", "name": "@getpaseo/client",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"@getpaseo/protocol": "0.1.109", "@getpaseo/protocol": "0.1.110",
"@getpaseo/relay": "0.1.109", "@getpaseo/relay": "0.1.110",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
@@ -36446,7 +36446,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@getpaseo/desktop", "name": "@getpaseo/desktop",
"version": "0.1.109", "version": "0.1.110",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@getpaseo/cli": "*", "@getpaseo/cli": "*",
@@ -36689,7 +36689,7 @@
}, },
"packages/expo-two-way-audio": { "packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio", "name": "@getpaseo/expo-two-way-audio",
"version": "0.1.109", "version": "0.1.110",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
@@ -37585,7 +37585,7 @@
}, },
"packages/highlight": { "packages/highlight": {
"name": "@getpaseo/highlight", "name": "@getpaseo/highlight",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.12.3", "@codemirror/language": "^6.12.3",
"@codemirror/legacy-modes": "^6.5.3", "@codemirror/legacy-modes": "^6.5.3",
@@ -37817,7 +37817,7 @@
}, },
"packages/protocol": { "packages/protocol": {
"name": "@getpaseo/protocol", "name": "@getpaseo/protocol",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
@@ -37830,7 +37830,7 @@
}, },
"packages/relay": { "packages/relay": {
"name": "@getpaseo/relay", "name": "@getpaseo/relay",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
@@ -38048,15 +38048,15 @@
}, },
"packages/server": { "packages/server": {
"name": "@getpaseo/server", "name": "@getpaseo/server",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"@agentclientprotocol/sdk": "^0.17.1", "@agentclientprotocol/sdk": "^0.17.1",
"@anthropic-ai/claude-agent-sdk": "^0.3.195", "@anthropic-ai/claude-agent-sdk": "^0.3.195",
"@anthropic-ai/sdk": "^0.104.2", "@anthropic-ai/sdk": "^0.104.2",
"@getpaseo/client": "0.1.109", "@getpaseo/client": "0.1.110",
"@getpaseo/highlight": "0.1.109", "@getpaseo/highlight": "0.1.110",
"@getpaseo/protocol": "0.1.109", "@getpaseo/protocol": "0.1.110",
"@getpaseo/relay": "0.1.109", "@getpaseo/relay": "0.1.110",
"@isaacs/ttlcache": "^2.1.4", "@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "^1.20.1", "@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.14.46", "@opencode-ai/sdk": "1.14.46",
@@ -38593,7 +38593,7 @@
}, },
"packages/website": { "packages/website": {
"name": "@getpaseo/website", "name": "@getpaseo/website",
"version": "0.1.109", "version": "0.1.110",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/vite-plugin": "^1.29.1",
"@cloudflare/workers-types": "^4.20260317.1", "@cloudflare/workers-types": "^4.20260317.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "paseo", "name": "paseo",
"version": "0.1.109", "version": "0.1.110",
"private": true, "private": true,
"description": "Paseo: voice-controlled development environment for local AI coding agents", "description": "Paseo: voice-controlled development environment for local AI coding agents",
"keywords": [ "keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/app", "name": "@getpaseo/app",
"version": "0.1.109", "version": "0.1.110",
"private": true, "private": true,
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/cli", "name": "@getpaseo/cli",
"version": "0.1.109", "version": "0.1.110",
"description": "Paseo CLI - control your AI coding agents from the command line", "description": "Paseo CLI - control your AI coding agents from the command line",
"bin": { "bin": {
"paseo": "bin/paseo" "paseo": "bin/paseo"
@@ -27,9 +27,9 @@
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "^1.0.0", "@clack/prompts": "^1.0.0",
"@getpaseo/client": "0.1.109", "@getpaseo/client": "0.1.110",
"@getpaseo/protocol": "0.1.109", "@getpaseo/protocol": "0.1.110",
"@getpaseo/server": "0.1.109", "@getpaseo/server": "0.1.110",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^12.0.0", "commander": "^12.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/client", "name": "@getpaseo/client",
"version": "0.1.109", "version": "0.1.110",
"description": "Paseo client SDK package", "description": "Paseo client SDK package",
"files": [ "files": [
"dist", "dist",
@@ -35,8 +35,8 @@
"test": "vitest run" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"@getpaseo/protocol": "0.1.109", "@getpaseo/protocol": "0.1.110",
"@getpaseo/relay": "0.1.109", "@getpaseo/relay": "0.1.110",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/desktop", "name": "@getpaseo/desktop",
"version": "0.1.109", "version": "0.1.110",
"private": true, "private": true,
"description": "Paseo desktop app (Electron wrapper)", "description": "Paseo desktop app (Electron wrapper)",
"homepage": "https://paseo.sh", "homepage": "https://paseo.sh",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/expo-two-way-audio", "name": "@getpaseo/expo-two-way-audio",
"version": "0.1.109", "version": "0.1.110",
"description": "Native module for two way audio streaming", "description": "Native module for two way audio streaming",
"keywords": [ "keywords": [
"ExpoTwoWayAudio", "ExpoTwoWayAudio",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/highlight", "name": "@getpaseo/highlight",
"version": "0.1.109", "version": "0.1.110",
"files": [ "files": [
"dist", "dist",
"!dist/**/*.map" "!dist/**/*.map"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/protocol", "name": "@getpaseo/protocol",
"version": "0.1.109", "version": "0.1.110",
"description": "Paseo shared protocol schemas and wire types", "description": "Paseo shared protocol schemas and wire types",
"files": [ "files": [
"dist", "dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/relay", "name": "@getpaseo/relay",
"version": "0.1.109", "version": "0.1.110",
"description": "Paseo relay for bridging daemon and client connections", "description": "Paseo relay for bridging daemon and client connections",
"files": [ "files": [
"dist", "dist",
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/server", "name": "@getpaseo/server",
"version": "0.1.109", "version": "0.1.110",
"description": "Paseo backend server", "description": "Paseo backend server",
"files": [ "files": [
"dist/server", "dist/server",
@@ -67,10 +67,10 @@
"@agentclientprotocol/sdk": "^0.17.1", "@agentclientprotocol/sdk": "^0.17.1",
"@anthropic-ai/claude-agent-sdk": "^0.3.195", "@anthropic-ai/claude-agent-sdk": "^0.3.195",
"@anthropic-ai/sdk": "^0.104.2", "@anthropic-ai/sdk": "^0.104.2",
"@getpaseo/client": "0.1.109", "@getpaseo/client": "0.1.110",
"@getpaseo/highlight": "0.1.109", "@getpaseo/highlight": "0.1.110",
"@getpaseo/protocol": "0.1.109", "@getpaseo/protocol": "0.1.110",
"@getpaseo/relay": "0.1.109", "@getpaseo/relay": "0.1.110",
"@isaacs/ttlcache": "^2.1.4", "@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "^1.20.1", "@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.14.46", "@opencode-ai/sdk": "1.14.46",
@@ -2208,8 +2208,7 @@ describe("ACPAgentSession", () => {
expect(assistantMessages[2].messageId).not.toBe(assistantMessages[0].messageId); expect(assistantMessages[2].messageId).not.toBe(assistantMessages[0].messageId);
}); });
test("starts an autonomous turn for spontaneous session updates outside a foreground turn", async () => { test("keeps ACP configuration notifications outside the turn lifecycle", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
const session = createSession(); const session = createSession();
asInternals<ACPSessionInternals>(session).sessionId = "session-1"; asInternals<ACPSessionInternals>(session).sessionId = "session-1";
@@ -2221,49 +2220,26 @@ describe("ACPAgentSession", () => {
await session.sessionUpdate({ await session.sessionUpdate({
sessionId: "session-1", sessionId: "session-1",
update: { update: {
sessionUpdate: "agent_message_chunk", sessionUpdate: "config_option_update",
messageId: "autonomous-msg", configOptions: [
content: { type: "text", text: "Autonomous update" }, selectConfigOption("mode", ["plan", "yolo"], "yolo"),
selectConfigOption("model", ["kimi-code/kimi-for-coding"]),
selectConfigOption("thought_level", ["off", "on"], "on"),
],
} as SessionUpdate, } as SessionUpdate,
}); });
// Should emit turn_started before the timeline item. expect(events.map((event) => event.type)).toEqual([
const turnStartedIndex = events.findIndex((e) => e.type === "turn_started"); "thread_started",
expect(turnStartedIndex).toBeGreaterThanOrEqual(0); "mode_changed",
const timelineIndex = events.findIndex( "model_changed",
(e) => "thinking_option_changed",
e.type === "timeline" && ]);
e.item.type === "assistant_message" &&
e.item.text === "Autonomous update",
);
expect(timelineIndex).toBeGreaterThan(turnStartedIndex);
const turnStarted = events[turnStartedIndex];
expect(turnStarted.type).toBe("turn_started");
const autonomousTurnId = (turnStarted as { turnId?: string }).turnId;
expect(autonomousTurnId).toEqual(expect.any(String));
// Timeline item should be tagged with the autonomous turn id.
const timelineEvent = events[timelineIndex];
expect(timelineEvent.type).toBe("timeline");
expect((timelineEvent as { turnId?: string }).turnId).toBe(autonomousTurnId);
// Advance timers to complete the autonomous turn.
await vi.advanceTimersByTimeAsync(ACPAgentSession["AUTONOMOUS_TURN_TIMEOUT_MS"] + 10);
const turnCompleted = events.find((e) => e.type === "turn_completed");
expect(turnCompleted).toBeDefined();
expect((turnCompleted as { turnId?: string }).turnId).toBe(autonomousTurnId);
vi.useRealTimers();
}); });
test("completes an existing autonomous turn before starting a foreground turn", async () => { test("forwards out-of-prompt ACP content without inventing a turn", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
const session = createSession(); const session = createSession();
asInternals<ACPSessionInternals>(session).sessionId = "session-1"; asInternals<ACPSessionInternals>(session).sessionId = "session-1";
asInternals<ACPSessionInternals>(session).connection = {
prompt: vi.fn(() => new Promise(() => {})),
};
const events: AgentStreamEvent[] = []; const events: AgentStreamEvent[] = [];
session.subscribe((event) => { session.subscribe((event) => {
@@ -2274,25 +2250,27 @@ describe("ACPAgentSession", () => {
sessionId: "session-1", sessionId: "session-1",
update: { update: {
sessionUpdate: "agent_message_chunk", sessionUpdate: "agent_message_chunk",
messageId: "autonomous-msg", messageId: "unscoped-message",
content: { type: "text", text: "Autonomous update" }, content: { type: "text", text: "Unscoped ACP update" },
} as SessionUpdate, } as SessionUpdate,
}); });
const turnStarted = events.find((e) => e.type === "turn_started"); expect(events).toEqual([
expect(turnStarted).toBeDefined(); {
const autonomousTurnId = (turnStarted as { turnId?: string }).turnId; type: "thread_started",
provider: "claude-acp",
// Starting a foreground turn should complete the autonomous turn first. sessionId: "session-1",
void session.startTurn("user prompt"); },
await vi.runOnlyPendingTimersAsync(); {
type: "timeline",
const turnCompleted = events.find( provider: "claude-acp",
(e) => e.type === "turn_completed" && (e as { turnId?: string }).turnId === autonomousTurnId, item: {
); type: "assistant_message",
expect(turnCompleted).toBeDefined(); text: "Unscoped ACP update",
messageId: "unscoped-message",
vi.useRealTimers(); },
},
]);
}); });
test("startTurn returns before the ACP prompt settles and completes later via subscribers", async () => { test("startTurn returns before the ACP prompt settles and completes later via subscribers", async () => {
@@ -1331,9 +1331,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
private readonly extensionCommandsParser?: ACPExtensionCommandsParser; private readonly extensionCommandsParser?: ACPExtensionCommandsParser;
private currentTurnUsage: AgentUsage | undefined; private currentTurnUsage: AgentUsage | undefined;
private activeForegroundTurnId: string | null = null; private activeForegroundTurnId: string | null = null;
private autonomousTurnId: string | null = null;
private autonomousTurnTimer: ReturnType<typeof setTimeout> | null = null;
private static readonly AUTONOMOUS_TURN_TIMEOUT_MS = 30_000;
private fallbackAssistantMessageId: string | null = null; private fallbackAssistantMessageId: string | null = null;
private closed = false; private closed = false;
private historyPending = false; private historyPending = false;
@@ -1475,7 +1472,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
if (this.activeForegroundTurnId) { if (this.activeForegroundTurnId) {
throw new Error("A foreground turn is already active"); throw new Error("A foreground turn is already active");
} }
this.completeAutonomousTurn();
const turnId = randomUUID(); const turnId = randomUUID();
const messageId = options?.messageId ?? randomUUID(); const messageId = options?.messageId ?? randomUUID();
@@ -2119,7 +2115,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
agentId: this.agentId, agentId: this.agentId,
provider: this.provider, provider: this.provider,
sessionId: this.sessionId, sessionId: this.sessionId,
turnId: this.activeForegroundTurnId ?? this.autonomousTurnId ?? undefined, turnId: this.activeForegroundTurnId ?? undefined,
rawEvent: params, rawEvent: params,
events, events,
}, },
@@ -2134,11 +2130,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
return; return;
} }
if (events.length > 0 && !this.activeForegroundTurnId) {
this.startAutonomousTurn();
this.resetAutonomousTurnTimer();
}
for (const event of events) { for (const event of events) {
this.pushEvent(event); this.pushEvent(event);
} }
@@ -2672,25 +2663,23 @@ export class ACPAgentSession implements AgentSession, ACPClient {
type: "timeline", type: "timeline",
provider: this.provider, provider: this.provider,
item, item,
turnId: this.activeForegroundTurnId ?? this.autonomousTurnId ?? undefined, turnId: this.activeForegroundTurnId ?? undefined,
}; };
} }
private pushEvent(event: AgentStreamEvent): void { private pushEvent(event: AgentStreamEvent): void {
const turnId = this.activeForegroundTurnId ?? this.autonomousTurnId;
const tagged = event.type === "timeline" && turnId ? { ...event, turnId } : event;
this.logger.trace( this.logger.trace(
{ {
agentId: this.agentId, agentId: this.agentId,
provider: this.provider, provider: this.provider,
sessionId: this.sessionId, sessionId: this.sessionId,
turnId: getAgentStreamEventTurnId(tagged) ?? turnId ?? undefined, turnId: getAgentStreamEventTurnId(event) ?? this.activeForegroundTurnId ?? undefined,
event: tagged, event,
}, },
"provider.acp.event_emit", "provider.acp.event_emit",
); );
for (const subscriber of this.subscribers) { for (const subscriber of this.subscribers) {
subscriber(tagged); subscriber(event);
} }
} }
@@ -2738,41 +2727,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
this.pushEvent(event); this.pushEvent(event);
} }
private startAutonomousTurn(): void {
if (this.autonomousTurnId) {
return;
}
this.autonomousTurnId = randomUUID();
this.pushEvent({
type: "turn_started",
provider: this.provider,
turnId: this.autonomousTurnId,
});
}
private completeAutonomousTurn(): void {
if (!this.autonomousTurnId) {
return;
}
if (this.autonomousTurnTimer) {
clearTimeout(this.autonomousTurnTimer);
this.autonomousTurnTimer = null;
}
const turnId = this.autonomousTurnId;
this.autonomousTurnId = null;
this.pushEvent({ type: "turn_completed", provider: this.provider, turnId });
}
private resetAutonomousTurnTimer(): void {
if (this.autonomousTurnTimer) {
clearTimeout(this.autonomousTurnTimer);
}
this.autonomousTurnTimer = setTimeout(() => {
this.completeAutonomousTurn();
}, ACPAgentSession.AUTONOMOUS_TURN_TIMEOUT_MS);
this.autonomousTurnTimer.unref?.();
}
private isSubmittedUserMessageEcho( private isSubmittedUserMessageEcho(
item: Extract<AgentTimelineItem, { type: "user_message" }>, item: Extract<AgentTimelineItem, { type: "user_message" }>,
): boolean { ): boolean {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@getpaseo/website", "name": "@getpaseo/website",
"version": "0.1.109", "version": "0.1.110",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {