From 9c6b33635be122462c71a61ffb4d4c01e878840e Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Mon, 8 Jun 2026 19:37:54 +0000 Subject: [PATCH 1/2] fix(compaction): stabilize deletion planner Replace the sqlite-backed transcript deletion store with an in-memory implementation so /compact works without native better-sqlite3 installs. Preserve the active reasoning level when auto-compaction invokes the planner and cover both behaviors with tests. Refs #1310 Assistant-model: GPT-5.5 --- bun.lock | 78 +--- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/package.json | 2 - .../coding-agent/src/core/agent-session.ts | 2 +- .../src/core/compaction/context-compaction.ts | 411 +++++++----------- ...gent-session-auto-compaction-queue.test.ts | 36 +- .../context-compaction-deletion-tool.test.ts | 8 +- 7 files changed, 196 insertions(+), 345 deletions(-) diff --git a/bun.lock b/bun.lock index 83c6b7d8d..5bef4a823 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ }, "packages/coding-agent": { "name": "@bastani/atomic", - "version": "0.8.26-alpha.11", + "version": "0.8.26", "bin": { "atomic": "dist/cli.js", }, @@ -24,7 +24,6 @@ "@modelcontextprotocol/sdk": "^1.25.1", "@mozilla/readability": "^0.6.0", "@silvia-odwyer/photon-node": "^0.3.4", - "better-sqlite3": "12.10.0", "chalk": "^5.5.0", "cross-spawn": "7.0.6", "diff": "^8.0.2", @@ -46,7 +45,6 @@ "zod": "^3.25.0 || ^4.0.0", }, "devDependencies": { - "@types/better-sqlite3": "7.6.13", "@types/cross-spawn": "6.0.6", "@types/diff": "^7.0.2", "@types/hosted-git-info": "^3.0.5", @@ -64,7 +62,7 @@ }, "packages/intercom": { "name": "@bastani/intercom", - "version": "0.8.26-alpha.11", + "version": "0.8.26", "dependencies": { "typebox": "^1.1.24", }, @@ -79,7 +77,7 @@ }, "packages/mcp": { "name": "@bastani/mcp", - "version": "0.8.26-alpha.11", + "version": "0.8.26", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/sdk": "^1.25.1", @@ -101,7 +99,7 @@ }, "packages/subagents": { "name": "@bastani/subagents", - "version": "0.8.26-alpha.11", + "version": "0.8.26", "dependencies": { "jiti": "^2.7.0", "typebox": "^1.1.24", @@ -121,7 +119,7 @@ }, "packages/web-access": { "name": "@bastani/web-access", - "version": "0.8.26-alpha.11", + "version": "0.8.26", "dependencies": { "@mozilla/readability": "^0.6.0", "linkedom": "^0.18.12", @@ -140,7 +138,7 @@ }, "packages/workflows": { "name": "@bastani/workflows", - "version": "0.8.26-alpha.11", + "version": "0.8.26", "dependencies": { "jiti": "^2.7.0", "typebox": "^1.1.24", @@ -454,8 +452,6 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -522,14 +518,8 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="], - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], - - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], @@ -540,8 +530,6 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -558,8 +546,6 @@ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -584,10 +570,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -596,8 +578,6 @@ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -642,8 +622,6 @@ "execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -668,8 +646,6 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -680,8 +656,6 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -698,8 +672,6 @@ "get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -734,14 +706,10 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], @@ -808,28 +776,20 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="], - "node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="], - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -882,8 +842,6 @@ "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], "protobufjs": ["protobufjs@7.5.8", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA=="], @@ -900,10 +858,6 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -926,7 +880,7 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="], + "semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -954,10 +908,6 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -966,20 +916,12 @@ "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="], @@ -996,8 +938,6 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turndown": ["turndown@7.2.4", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], @@ -1016,8 +956,6 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="], @@ -1078,8 +1016,6 @@ "execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], - "execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "execa/cross-spawn/shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="], "execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 91e34ffda..c5fd3528d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/compact` and auto-compaction regressions by removing the native `better-sqlite3` dependency from transcript-bound deletion tools and preserving the currently selected reasoning level for the compaction planner ([#1310](https://github.com/bastani-inc/atomic/issues/1310)). + ## [0.8.26] - 2026-06-08 ### Added diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index f7b50a9bd..fd01f6c03 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -74,7 +74,6 @@ "@modelcontextprotocol/sdk": "^1.25.1", "@mozilla/readability": "^0.6.0", "@silvia-odwyer/photon-node": "^0.3.4", - "better-sqlite3": "12.10.0", "chalk": "^5.5.0", "cross-spawn": "7.0.6", "diff": "^8.0.2", @@ -105,7 +104,6 @@ "@mariozechner/clipboard": "^0.3.6" }, "devDependencies": { - "@types/better-sqlite3": "7.6.13", "@types/cross-spawn": "6.0.6", "@types/diff": "^7.0.2", "@types/hosted-git-info": "^3.0.5", diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 76bbd7d2e..fb9833a9e 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1984,7 +1984,7 @@ export class AgentSession { options.apiKey, options.headers, options.abortController.signal, - undefined, + this.thinkingLevel, mode, ); diff --git a/packages/coding-agent/src/core/compaction/context-compaction.ts b/packages/coding-agent/src/core/compaction/context-compaction.ts index c5aeb7560..f2f7ef835 100644 --- a/packages/coding-agent/src/core/compaction/context-compaction.ts +++ b/packages/coding-agent/src/core/compaction/context-compaction.ts @@ -2,13 +2,11 @@ import { Agent, type AgentMessage, type AgentTool, type AgentToolResult, type Th import type { Api, AssistantMessage, Model, ToolCall } from "@earendil-works/pi-ai"; import { createAssistantMessageEventStream, - getSupportedThinkingLevels, isContextOverflow, streamSimple, StringEnum, } from "@earendil-works/pi-ai"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Type } from "typebox"; @@ -29,8 +27,6 @@ import { estimateTokens } from "./compaction.ts"; export const CONTEXT_COMPACTION_PROMPT_VERSION = 1 as const; -const CONTEXT_COMPACTION_THINKING_LEVEL_ORDER: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; - export type ContextCompactionMode = "standard" | "critical_overflow"; export interface ContextDeletionRequest { @@ -1187,96 +1183,7 @@ function addGrepCandidate( matches.push(match); } -type SqliteValue = string | number | bigint | null; -type SqliteRow = Record; - -interface SqliteStatementLike { - all(...params: SqliteValue[]): SqliteRow[]; - get(...params: SqliteValue[]): SqliteRow | undefined; - run(...params: SqliteValue[]): unknown; -} - -interface SqliteDatabaseLike { - exec(sql: string): void; - prepare(sql: string): SqliteStatementLike; - close(): unknown; -} - -interface SqliteConstructorLike { - new (filename: string): SqliteDatabaseLike; -} - -interface BunSqliteModule { - Database: SqliteConstructorLike; -} - -const moduleRequire = createRequire(import.meta.url); - -function createTransientSqliteDatabase(): SqliteDatabaseLike { - // better-sqlite3 is the portable/package dependency for Node-based Atomic installs. - // Bun cannot dlopen better-sqlite3 yet, so Bun runtime/tests use the API-compatible builtin. - if (process.versions.bun) { - const sqlite = moduleRequire("bun:sqlite") as BunSqliteModule; - return new sqlite.Database(":memory:"); - } - - const BetterSqliteDatabase = moduleRequire("better-sqlite3") as SqliteConstructorLike; - return new BetterSqliteDatabase(":memory:"); -} - -class SqliteAdapter { - private readonly db: SqliteDatabaseLike; - - constructor(db: SqliteDatabaseLike) { - this.db = db; - } - - static createTransient(): SqliteAdapter { - return new SqliteAdapter(createTransientSqliteDatabase()); - } - - exec(sql: string): void { - this.db.exec(sql); - } - - run(sql: string, ...params: SqliteValue[]): void { - this.db.prepare(sql).run(...params); - } - - all(sql: string, ...params: SqliteValue[]): TRow[] { - return this.db.prepare(sql).all(...params) as TRow[]; - } - - get(sql: string, ...params: SqliteValue[]): TRow | undefined { - return this.db.prepare(sql).get(...params) as TRow | undefined; - } - - transaction(operation: () => T): T { - this.exec("BEGIN IMMEDIATE"); - try { - const result = operation(); - this.exec("COMMIT"); - return result; - } catch (error) { - try { - this.exec("ROLLBACK"); - } catch {} - throw error; - } - } - - close(): void { - this.db.close(); - } -} - -interface DeletionTargetRow extends SqliteRow { - kind: "entry" | "content_block"; - entry_id: string; - block_index: number | null; -} - -interface EntryTextRow extends SqliteRow { +interface EntryTextRow { entry_id: string; text: string; is_protected: number; @@ -1287,7 +1194,7 @@ interface EntryReadRow extends EntryTextRow { token_estimate: number; } -interface ContentBlockTextRow extends SqliteRow { +interface ContentBlockTextRow { entry_id: string; block_index: number; text: string; @@ -1301,211 +1208,198 @@ interface ContentBlockReadRow extends ContentBlockTextRow { token_estimate: number; } -class ContextDeletionSqliteStore { - private readonly sqlite: SqliteAdapter; +interface StoredTranscriptEntry { + entryId: string; + role: AgentMessage["role"]; + protected: boolean; + tokenEstimate: number; + text: string; +} - constructor(sqlite: SqliteAdapter) { - this.sqlite = sqlite; - } +interface StoredContentBlock { + entryPosition: number; + entryId: string; + blockIndex: number; + type: string; + protected: boolean; + tokenEstimate: number; + text: string; +} - initialize(transcript: CompactableTranscript): void { - this.sqlite.transaction(() => { - this.sqlite.exec(` - PRAGMA foreign_keys = ON; - CREATE TABLE transcript_entries ( - position INTEGER PRIMARY KEY, - entry_id TEXT NOT NULL UNIQUE, - role TEXT NOT NULL, - is_protected INTEGER NOT NULL, - token_estimate INTEGER NOT NULL, - text TEXT NOT NULL, - tool_result_for TEXT - ); - CREATE TABLE transcript_content_blocks ( - entry_id TEXT NOT NULL, - block_index INTEGER NOT NULL, - type TEXT NOT NULL, - is_protected INTEGER NOT NULL, - token_estimate INTEGER NOT NULL, - text TEXT NOT NULL, - tool_call_id TEXT, - PRIMARY KEY (entry_id, block_index), - FOREIGN KEY (entry_id) REFERENCES transcript_entries(entry_id) ON DELETE CASCADE - ); - CREATE TABLE deletion_targets ( - position INTEGER PRIMARY KEY AUTOINCREMENT, - target_key TEXT NOT NULL UNIQUE, - kind TEXT NOT NULL CHECK (kind IN ('entry', 'content_block')), - entry_id TEXT NOT NULL, - block_index INTEGER - ); - CREATE TABLE context_compaction_state ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL - ); - INSERT INTO context_compaction_state (key, value) VALUES ('call_count', '0'); - `); - - for (const [position, entry] of transcript.entries.entries()) { - this.sqlite.run( - "INSERT INTO transcript_entries (position, entry_id, role, is_protected, token_estimate, text, tool_result_for) VALUES (?, ?, ?, ?, ?, ?, ?)", - position, - entry.entryId, - entry.role, - entry.protected ? 1 : 0, - entry.tokenEstimate, - entry.text, - entry.toolResultFor ?? null, - ); - for (const block of entry.contentBlocks) { - this.sqlite.run( - "INSERT INTO transcript_content_blocks (entry_id, block_index, type, is_protected, token_estimate, text, tool_call_id) VALUES (?, ?, ?, ?, ?, ?, ?)", - block.entryId, - block.blockIndex, - block.type, - block.protected ? 1 : 0, - block.tokenEstimate, - block.text, - block.toolCallId ?? null, - ); - } +interface ContextDeletionMemorySnapshot { + deletionTargets: ContextDeletionTarget[]; + callCount: number; + lastError?: string; +} + +function copyDeletionTarget(target: ContextDeletionTarget): ContextDeletionTarget { + return target.kind === "entry" + ? { kind: "entry", entryId: target.entryId } + : { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex }; +} + +class ContextDeletionMemoryStore { + private readonly entries: StoredTranscriptEntry[]; + private readonly entriesById: Map; + private readonly contentBlocks: StoredContentBlock[]; + private readonly contentBlockCountByEntryId: Map; + private deletionTargets: ContextDeletionTarget[] = []; + private callCount = 0; + private lastError: string | undefined; + + constructor(transcript: CompactableTranscript) { + const entryIds = new Set(); + const blockKeys = new Set(); + this.entries = transcript.entries.map((entry) => { + if (entryIds.has(entry.entryId)) { + throw new Error(`Duplicate transcript entry id: ${entry.entryId}`); } + entryIds.add(entry.entryId); + return { + entryId: entry.entryId, + role: entry.role, + protected: entry.protected, + tokenEstimate: entry.tokenEstimate, + text: entry.text, + }; }); + this.entriesById = new Map(this.entries.map((entry) => [entry.entryId, entry] as const)); + this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => + entry.contentBlocks.map((block) => { + if (block.entryId !== entry.entryId) { + throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`); + } + const blockKey = `${block.entryId}:${block.blockIndex}`; + if (blockKeys.has(blockKey)) { + throw new Error(`Duplicate transcript content block: ${blockKey}`); + } + blockKeys.add(blockKey); + return { + entryPosition, + entryId: block.entryId, + blockIndex: block.blockIndex, + type: block.type, + protected: block.protected, + tokenEstimate: block.tokenEstimate, + text: block.text, + }; + }), + ); + this.contentBlockCountByEntryId = new Map(); + for (const block of this.contentBlocks) { + this.contentBlockCountByEntryId.set(block.entryId, (this.contentBlockCountByEntryId.get(block.entryId) ?? 0) + 1); + } } transaction(operation: () => T): T { - return this.sqlite.transaction(operation); + const snapshot = this.snapshot(); + try { + return operation(); + } catch (error) { + this.restore(snapshot); + throw error; + } } readTargets(): ContextDeletionTarget[] { - return this.sqlite - .all("SELECT kind, entry_id, block_index FROM deletion_targets ORDER BY position") - .map((row) => - row.kind === "entry" - ? { kind: "entry", entryId: row.entry_id } - : { kind: "content_block", entryId: row.entry_id, blockIndex: row.block_index as number }, - ); + return this.deletionTargets.map(copyDeletionTarget); } replaceTargets(targets: readonly ContextDeletionTarget[]): void { - this.sqlite.run("DELETE FROM deletion_targets"); - for (const target of targets) { - this.sqlite.run( - "INSERT INTO deletion_targets (target_key, kind, entry_id, block_index) VALUES (?, ?, ?, ?)", - targetKey(target), - target.kind, - target.entryId, - target.kind === "content_block" ? target.blockIndex : null, - ); - } + this.deletionTargets = targets.map(copyDeletionTarget); } listEntriesForGrep(): EntryTextRow[] { - return this.sqlite.all( - "SELECT entry_id, text, is_protected FROM transcript_entries ORDER BY position", - ); + return this.entries.map((entry) => ({ + entry_id: entry.entryId, + text: entry.text, + is_protected: entry.protected ? 1 : 0, + })); } listContentBlocksForGrep(): ContentBlockTextRow[] { - return this.sqlite.all(` - SELECT - blocks.entry_id, - blocks.block_index, - blocks.text, - entries.is_protected AS entry_protected, - blocks.is_protected AS block_protected, - ( - SELECT COUNT(*) - FROM transcript_content_blocks sibling - WHERE sibling.entry_id = blocks.entry_id - ) AS block_count - FROM transcript_content_blocks blocks - JOIN transcript_entries entries ON entries.entry_id = blocks.entry_id - ORDER BY entries.position, blocks.block_index - `); + return [...this.contentBlocks] + .sort((a, b) => a.entryPosition - b.entryPosition || a.blockIndex - b.blockIndex) + .map((block) => ({ + entry_id: block.entryId, + block_index: block.blockIndex, + text: block.text, + entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0, + block_protected: block.protected ? 1 : 0, + block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0, + })); } getEntryForRead(entryId: string): EntryReadRow | undefined { - return this.sqlite.get( - "SELECT entry_id, role, is_protected, token_estimate, text FROM transcript_entries WHERE entry_id = ?", - entryId, - ); + const entry = this.entriesById.get(entryId); + if (!entry) return undefined; + return { + entry_id: entry.entryId, + role: entry.role, + is_protected: entry.protected ? 1 : 0, + token_estimate: entry.tokenEstimate, + text: entry.text, + }; } getContentBlockForRead(entryId: string, blockIndex: number): ContentBlockReadRow | undefined { - return this.sqlite.get( - ` - SELECT - blocks.entry_id, - blocks.block_index, - blocks.type, - blocks.token_estimate, - blocks.text, - entries.is_protected AS entry_protected, - blocks.is_protected AS block_protected, - ( - SELECT COUNT(*) - FROM transcript_content_blocks sibling - WHERE sibling.entry_id = blocks.entry_id - ) AS block_count - FROM transcript_content_blocks blocks - JOIN transcript_entries entries ON entries.entry_id = blocks.entry_id - WHERE blocks.entry_id = ? AND blocks.block_index = ? - `, - entryId, - blockIndex, - ); + const block = this.contentBlocks.find((candidate) => candidate.entryId === entryId && candidate.blockIndex === blockIndex); + if (!block) return undefined; + return { + entry_id: block.entryId, + block_index: block.blockIndex, + type: block.type, + token_estimate: block.tokenEstimate, + text: block.text, + entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0, + block_protected: block.protected ? 1 : 0, + block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0, + }; } getGrepScanTextLength(target: "entry" | "content_block"): number { - const table = target === "entry" ? "transcript_entries" : "transcript_content_blocks"; - const row = this.sqlite.get<{ scan_chars: number | null }>(`SELECT SUM(LENGTH(text)) AS scan_chars FROM ${table}`); - return row?.scan_chars ?? 0; + const texts = target === "entry" ? this.entries : this.contentBlocks; + return texts.reduce((sum, item) => sum + item.text.length, 0); } incrementCallCount(): number { - const next = this.getCallCount() + 1; - this.setState("call_count", String(next)); - return next; + this.callCount += 1; + return this.callCount; } getCallCount(): number { - return Number(this.getState("call_count") ?? "0"); + return this.callCount; } setLastError(message: string): void { - this.setState("last_error", message); + this.lastError = message; } clearLastError(): void { - this.sqlite.run("DELETE FROM context_compaction_state WHERE key = ?", "last_error"); + this.lastError = undefined; } getLastError(): string | undefined { - return this.getState("last_error"); - } - - private getState(key: string): string | undefined { - return this.sqlite.get<{ value: string }>("SELECT value FROM context_compaction_state WHERE key = ?", key)?.value; + return this.lastError; } - private setState(key: string, value: string): void { - this.sqlite.run( - "INSERT INTO context_compaction_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", - key, - value, - ); + private snapshot(): ContextDeletionMemorySnapshot { + return { + deletionTargets: this.readTargets(), + callCount: this.callCount, + ...(this.lastError === undefined ? {} : { lastError: this.lastError }), + }; } - close(): void { - this.sqlite.close(); + private restore(snapshot: ContextDeletionMemorySnapshot): void { + this.deletionTargets = snapshot.deletionTargets.map(copyDeletionTarget); + this.callCount = snapshot.callCount; + this.lastError = snapshot.lastError; } } -function createContextDeletionSqliteStore(transcript: CompactableTranscript): ContextDeletionSqliteStore { - const store = new ContextDeletionSqliteStore(SqliteAdapter.createTransient()); - store.initialize(transcript); - return store; +function createContextDeletionStore(transcript: CompactableTranscript): ContextDeletionMemoryStore { + return new ContextDeletionMemoryStore(transcript); } export function createContextDeletionTool( @@ -1513,7 +1407,7 @@ export function createContextDeletionTool( options: ContextCompactionRunOptions = {}, ): ContextDeletionToolController { const mode = options.mode ?? "standard"; - const store = createContextDeletionSqliteStore(transcript); + const store = createContextDeletionStore(transcript); let validatedResult: ValidatedContextDeletionResult | undefined; function readTargets(): ContextDeletionTarget[] { @@ -2073,14 +1967,6 @@ function isContextCompactionOverflowError(model: Model, errorMessage: strin ); } -export function getLowestContextCompactionThinkingLevel(model: Model): ThinkingLevel { - const supportedLevels = getSupportedThinkingLevels(model) as ThinkingLevel[]; - for (const level of CONTEXT_COMPACTION_THINKING_LEVEL_ORDER) { - if (supportedLevels.includes(level)) return level; - } - return "off"; -} - interface ContextDeletionRun { validatedResult: ValidatedContextDeletionResult | undefined; lastToolError: string | undefined; @@ -2092,6 +1978,7 @@ async function runContextDeletionAssistant( apiKey: string, headers?: Record, signal?: AbortSignal, + thinkingLevel: ThinkingLevel = "off", mode: ContextCompactionMode = "standard", ): Promise { const maxTokens = Math.min(4096, model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); @@ -2105,13 +1992,12 @@ async function runContextDeletionAssistant( timestamp: Date.now(), }; const deletionTool = createContextDeletionTool(transcript, { mode }); - const effectiveThinkingLevel = getLowestContextCompactionThinkingLevel(model); let compactionTurnCount = 0; const agent = new Agent({ initialState: { systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT, model, - thinkingLevel: effectiveThinkingLevel, + thinkingLevel, tools: deletionTool.tools, }, toolExecution: "parallel", @@ -2170,7 +2056,7 @@ export async function contextCompact( apiKey: string, headers?: Record, signal?: AbortSignal, - _thinkingLevel?: ThinkingLevel, + thinkingLevel: ThinkingLevel = "off", mode: ContextCompactionMode = preparation.mode ?? "standard", ): Promise { const { validatedResult, lastToolError } = await runContextDeletionAssistant( @@ -2179,6 +2065,7 @@ export async function contextCompact( apiKey, headers, signal, + thinkingLevel, mode, ); if (!validatedResult || validatedResult.deletedTargets.length === 0) { diff --git a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts index 9900f89b8..51b44351a 100644 --- a/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts +++ b/packages/coding-agent/test/agent-session-auto-compaction-queue.test.ts @@ -22,6 +22,21 @@ function createContextCompactionStats(tokensBefore: number, tokensAfter: number) }; } +const compactionMocks = vi.hoisted(() => ({ + contextCompact: vi.fn(async (..._args: unknown[]) => ({ + deletedTargets: [{ kind: "entry", entryId: "entry-1" }], + protectedEntryIds: [], + stats: { + objectsBefore: 1, + objectsAfter: 1, + objectsDeleted: 0, + tokensBefore: 100, + tokensAfter: 50, + percentReduction: 50, + }, + })), +})); + vi.mock("../src/core/compaction/index.js", () => ({ calculateContextTokens: (usage: { input: number; @@ -37,11 +52,7 @@ vi.mock("../src/core/compaction/index.js", () => ({ tokensBefore: 100, details: {}, }), - contextCompact: async () => ({ - deletedTargets: [{ kind: "entry", entryId: "entry-1" }], - protectedEntryIds: [], - stats: createContextCompactionStats(100, 50), - }), + contextCompact: compactionMocks.contextCompact, estimateContextTokens: ( messages: Array<{ role: string; @@ -76,6 +87,7 @@ describe("AgentSession auto-compaction queue resume", () => { let tempDir: string; beforeEach(() => { + compactionMocks.contextCompact.mockClear(); tempDir = join(tmpdir(), `pi-auto-compaction-queue-${Date.now()}`); mkdirSync(tempDir, { recursive: true }); vi.useFakeTimers(); @@ -114,6 +126,20 @@ describe("AgentSession auto-compaction queue resume", () => { } }); + it("passes the current thinking level to auto context compaction", async () => { + session.agent.state.thinkingLevel = "high"; + const runAutoCompaction = ( + session as unknown as { + _runAutoCompaction: (reason: "overflow" | "threshold", willRetry: boolean) => Promise; + } + )._runAutoCompaction.bind(session); + + await runAutoCompaction("threshold", false); + + expect(compactionMocks.contextCompact).toHaveBeenCalledTimes(1); + expect(compactionMocks.contextCompact.mock.calls[0]?.[5]).toBe("high"); + }); + it("should resume after threshold compaction when only agent-level queued messages exist", async () => { session.agent.followUp({ role: "custom", diff --git a/packages/coding-agent/test/context-compaction-deletion-tool.test.ts b/packages/coding-agent/test/context-compaction-deletion-tool.test.ts index c0aa2bb03..71bc5712e 100644 --- a/packages/coding-agent/test/context-compaction-deletion-tool.test.ts +++ b/packages/coding-agent/test/context-compaction-deletion-tool.test.ts @@ -673,7 +673,7 @@ describe("context compaction deletion tools", () => { ).rejects.toThrow(/Context compaction failed: 529 overloaded/); }); - it("uses the lowest supported thinking level for context compaction", async () => { + it("uses the selected thinking level for context compaction", async () => { let capturedReasoning: string | undefined; const faux = registerFauxProvider({ models: [{ id: "faux-reasoning", reasoning: true }] }); cleanups.push(() => faux.unregister()); @@ -694,10 +694,10 @@ describe("context compaction deletion tools", () => { await contextCompact({ transcript: createTranscript(), branchEntries: [] }, faux.getModel(), "test-key", undefined, undefined, "high"); - expect(capturedReasoning).toBeUndefined(); + expect(capturedReasoning).toBe("high"); }); - it("uses minimal thinking for context compaction when off is unsupported", async () => { + it("does not downgrade the selected thinking level when off is unsupported", async () => { let capturedReasoning: string | undefined; const faux = registerFauxProvider({ models: [{ id: "faux-reasoning-minimal", reasoning: true }] }); cleanups.push(() => faux.unregister()); @@ -719,7 +719,7 @@ describe("context compaction deletion tools", () => { await contextCompact({ transcript: createTranscript(), branchEntries: [] }, model, "test-key", undefined, undefined, "high"); - expect(capturedReasoning).toBe("minimal"); + expect(capturedReasoning).toBe("high"); }); it("surfaces the last deletion tool error when context compaction has no safe deletions", async () => { From 7875382a238a708fe2771b70e5d812f6271e5d5c Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Mon, 8 Jun 2026 19:56:53 +0000 Subject: [PATCH 2/2] fix(compaction): use model output cap for context deletion Assistant-model: GPT-5.5 --- packages/coding-agent/src/core/compaction/context-compaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/compaction/context-compaction.ts b/packages/coding-agent/src/core/compaction/context-compaction.ts index f2f7ef835..57d91441f 100644 --- a/packages/coding-agent/src/core/compaction/context-compaction.ts +++ b/packages/coding-agent/src/core/compaction/context-compaction.ts @@ -1981,7 +1981,7 @@ async function runContextDeletionAssistant( thinkingLevel: ThinkingLevel = "off", mode: ContextCompactionMode = "standard", ): Promise { - const maxTokens = Math.min(4096, model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); + const maxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY; if (signal?.aborted) { throw new Error("Context compaction failed: Request was aborted"); }