diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1df76ea..d3d64bb 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -5,7 +5,7 @@ "description": "Electron + React desktop MVP for Coding Agent", "main": "dist-electron/main/main.js", "scripts": { - "dev": "pnpm run build:electron && concurrently -k -n renderer,electron-ts,electron \"vite --host 127.0.0.1\" \"tsc -p tsconfig.electron.json --watch --preserveWatchOutput\" \"wait-on tcp:127.0.0.1:5173 && cross-env VITE_DEV_SERVER_URL=http://127.0.0.1:5173 electron .\"", + "dev": "pnpm run build:electron && concurrently -k -n renderer,electron-ts,electron \"vite --host 127.0.0.1\" \"tsc -p tsconfig.electron.json --watch --preserveWatchOutput\" \"wait-on tcp:127.0.0.1:5173 && cross-env ELECTRON_GET_USE_PROXY=true VITE_DEV_SERVER_URL=http://127.0.0.1:5173 electron .\"", "build:electron": "tsc -p tsconfig.electron.json", "build:renderer": "vite build", "build": "pnpm run build:electron && pnpm run build:renderer", diff --git a/apps/desktop/pnpm-lock.yaml b/apps/desktop/pnpm-lock.yaml index a64f2b8..dd17fda 100644 --- a/apps/desktop/pnpm-lock.yaml +++ b/apps/desktop/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + yauzl: ^3.3.1 + importers: .: @@ -378,9 +381,6 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -513,9 +513,6 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1112,8 +1109,9 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -1335,8 +1333,6 @@ snapshots: bail@2.0.2: {} - buffer-crc32@0.2.13: {} - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -1457,16 +1453,12 @@ snapshots: dependencies: debug: 4.4.3(supports-color@10.2.2) get-stream: 5.2.0 - yauzl: 2.10.0 + yauzl: 3.4.0 optionalDependencies: '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -2303,9 +2295,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 - yauzl@2.10.0: + yauzl@3.4.0: dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 + pend: 1.2.0 zwitch@2.0.4: {} diff --git a/apps/desktop/pnpm-workspace.yaml b/apps/desktop/pnpm-workspace.yaml index 29abe3b..bdc7340 100644 --- a/apps/desktop/pnpm-workspace.yaml +++ b/apps/desktop/pnpm-workspace.yaml @@ -1,2 +1,5 @@ onlyBuiltDependencies: - electron + +overrides: + yauzl: ^3.3.1 diff --git a/packages/app/src/coding_agent/core/agent_session.py b/packages/app/src/coding_agent/core/agent_session.py index 3b33a1d..43325c0 100644 --- a/packages/app/src/coding_agent/core/agent_session.py +++ b/packages/app/src/coding_agent/core/agent_session.py @@ -355,7 +355,7 @@ def new_session(self) -> SessionManager: leak into the old JSONL file. """ previous = self.session_manager - if not previous.in_memory: + if not previous.in_memory and previous.entries: previous.flush() new_manager = SessionManager.create( @@ -552,8 +552,12 @@ def get_stats(self) -> SessionStats: # ── Lifecycle ───────────────────────────────────────────────────────── def dispose(self) -> None: - """Clean up resources. Flushes the session to disk if needed.""" - if self.session_manager is not None and not self.session_manager.in_memory: + """Clean up resources, persisting only sessions that contain entries.""" + if ( + self.session_manager is not None + and not self.session_manager.in_memory + and self.session_manager.entries + ): self.session_manager.flush() def _restore_persisted_context(self) -> None: diff --git a/packages/app/tests/test_desktop_protocol.py b/packages/app/tests/test_desktop_protocol.py index d196d68..fee3fe8 100644 --- a/packages/app/tests/test_desktop_protocol.py +++ b/packages/app/tests/test_desktop_protocol.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +from agent_core import SessionManager from agent_llm import AssistantMessage, Model, ModelCost, TextContent, ToolCall, UserMessage from coding_agent.core.agent_session import AgentSession, AgentSessionConfig @@ -91,3 +92,43 @@ def test_desktop_command_catalog_only_exposes_supported_commands() -> None: assert [command["name"] for command in commands] == [ "help", "clear", "model", "compact", "session", "new", ] + + +def test_opening_saved_session_does_not_persist_abandoned_empty_session( + tmp_path: Path, +) -> None: + import asyncio + import coding_agent.core.config as config + + workspace = Path(tmp_path.anchor) + + saved = SessionManager.create( + cwd=str(workspace), + sessions_dir=config.get_sessions_dir(), + ) + saved.append_message(UserMessage(content="existing question")) + saved.append_message(AssistantMessage(content=[TextContent(text="existing answer")])) + + async def exercise() -> None: + runtime = DesktopRuntime(lambda _event: None) + try: + opened = await runtime.dispatch( + "workspace.open", + {"path": str(workspace), "resume": True}, + ) + assert opened["sessionId"] == saved.header.id + + created = await runtime.dispatch("session.new", {}) + assert created["sessionId"] != saved.header.id + assert [item["id"] for item in await runtime.dispatch("session.list", {})] == [ + saved.header.id, + ] + + await runtime.dispatch("session.open", {"sessionId": saved.header.id}) + assert [item["id"] for item in await runtime.dispatch("session.list", {})] == [ + saved.header.id, + ] + finally: + await runtime.dispose() + + asyncio.run(exercise()) diff --git a/packages/app/tests/test_release_p0.py b/packages/app/tests/test_release_p0.py index 1f971f1..b94c837 100644 --- a/packages/app/tests/test_release_p0.py +++ b/packages/app/tests/test_release_p0.py @@ -52,6 +52,16 @@ def test_new_session_switches_manager_and_storage(tmp_path: Path): assert old.path.exists() +def test_new_session_does_not_persist_empty_previous_manager(tmp_path: Path): + old = SessionManager.create(cwd=str(tmp_path), agent_dir=tmp_path) + session = AgentSession(AgentSessionConfig(model=_model(), tools=[], session_manager=old)) + + session.new_session() + + assert old.path is not None + assert not old.path.exists() + + def test_model_shorthand_sets_provider_and_thinking(): args = Args(model="zhipu/glm-5.2:high") _normalize_model_options(args)