add compaction (#183)

This commit is contained in:
Ismail Pelaseyed authored and GitHub committed 2026-03-21 00:03:49 +01:00
1 parent 7fc229e457
commit c722104d6f
11 files changed
+1197 -167

No files matched your search

+2 -2
View File
@@ -139,9 +139,9 @@ Treat the bot token like a password.
---
## MCP & project settings
## Project settings
Project file: **`.grok/settings.json`** — e.g. current model, **`mcpServers`** for stdio/http/sse MCP servers. Manage interactively with **`/mcps`** in the TUI.
Project file: **`.grok/settings.json`** — e.g. the current model for this project.
---
+145
View File
@@ -26,6 +26,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"typescript": "^5.9.3",
"vitest": "^4.1.0",
},
},
},
@@ -62,6 +63,12 @@
"@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="],
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@grammyjs/types": ["@grammyjs/types@3.25.0", "", {}, "sha512-iN9i5p+8ZOu9OMxWNcguojQfz4K/PDyMPOnL7PPCON+SoA/F8OKMH3uR7CVUkYfdNe0GCz8QOzAWrnqusQYFOg=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
@@ -122,8 +129,12 @@
"@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.88", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.88", "@opentui/core-darwin-x64": "0.1.88", "@opentui/core-linux-arm64": "0.1.88", "@opentui/core-linux-x64": "0.1.88", "@opentui/core-win32-arm64": "0.1.88", "@opentui/core-win32-x64": "0.1.88", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-eaDVZfAzZraddOIkgWSHMVkyaY0O20foYnPWKPQx1TY4t7G1oatIoan2zkytx67epW+4BZQ9vGib+61/uNM1MA=="],
@@ -142,18 +153,74 @@
"@opentui/react": ["@opentui/react@0.1.88", "", { "dependencies": { "@opentui/core": "0.1.88", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-EfEUBlGgCx9LqpbB852L+m7F19NcR8yeUww+C8NytIQeVKJbFIhSLu3r3wjBEGNoYiRk1DtAwWBpG5YaVvkXXw=="],
"@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="],
"@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="],
"@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="],
"@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="],
"@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="],
"@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@@ -174,6 +241,8 @@
"any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
@@ -202,6 +271,8 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="],
@@ -214,6 +285,8 @@
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -228,6 +301,8 @@
"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.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
@@ -246,10 +321,14 @@
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
@@ -264,6 +343,8 @@
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
"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=="],
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
@@ -272,6 +353,8 @@
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"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=="],
@@ -280,6 +363,8 @@
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
@@ -334,12 +419,38 @@
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lint-staged": ["lint-staged@16.4.0", "", { "dependencies": { "commander": "^14.0.3", "listr2": "^9.0.5", "picomatch": "^4.0.3", "string-argv": "^0.3.2", "tinyexec": "^1.0.4", "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw=="],
"listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="],
"log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -358,6 +469,8 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
@@ -366,6 +479,8 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
@@ -388,8 +503,12 @@
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="],
@@ -400,6 +519,8 @@
"pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
@@ -426,6 +547,8 @@
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -456,16 +579,24 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"simple-xml-to-json": ["simple-xml-to-json@1.2.4", "", {}, "sha512-3MY16e0ocMHL7N1ufpdObURGyX+lCo0T/A+y6VCwosLdH1HSda4QZl1Sdt/O+2qWp48WFi26XEp5rF0LoaL0Dg=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"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=="],
"stage-js": ["stage-js@1.0.1", "", {}, "sha512-cz14aPp/wY0s3bkb/B93BPP5ZAEhgBbRmAT3CCDqert8eCAqIpQ0RB2zpK8Ksxf+Pisl5oTzvPHtL4CVzzeHcw=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
@@ -478,16 +609,24 @@
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@@ -500,6 +639,10 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="],
"vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="],
"web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -508,6 +651,8 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+4 -1
View File
@@ -18,6 +18,8 @@
"build": "tsc",
"start": "bun run dist/index.js",
"typecheck": "tsc --noEmit",
"test": "bunx vitest run",
"test:watch": "bunx vitest",
"lint": "biome check src/",
"format": "biome format src/",
"format:fix": "biome format --write src/",
@@ -60,7 +62,8 @@
"@types/react": "^19.2.14",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.1.0"
},
"engines": {
"node": ">=18.0.0"
+236 -141
View File
@@ -1,13 +1,17 @@
import { type ModelMessage, stepCountIs, streamText, type ToolSet } from "ai";
import { createProvider, generateTitle as genTitle, type XaiProvider } from "../grok/client";
import { getModelInfo } from "../grok/models";
import { createTools } from "../grok/tools";
import { buildMcpToolSet } from "../mcp/runtime";
import {
appendCompaction,
appendMessages,
appendSystemMessage,
buildChatEntries,
getNextMessageSequence,
getSessionTotalTokens,
loadTranscript,
loadTranscriptState,
recordUsageEvent,
SessionStore,
} from "../storage/index";
@@ -29,6 +33,17 @@ import type {
import { loadCustomInstructions } from "../utils/instructions";
import { loadMcpServers } from "../utils/settings";
import { discoverSkills, formatSkillsForPrompt } from "../utils/skills";
import {
type CompactionSettings,
createCompactionSummaryMessage,
DEFAULT_KEEP_RECENT_TOKENS,
DEFAULT_RESERVE_TOKENS,
estimateConversationTokens,
generateCompactionSummary,
prepareCompaction,
relaxCompactionSettings,
shouldCompactContext,
} from "./compaction";
import { DelegationManager } from "./delegations";
const MAX_TOOL_ROUNDS = 400;
@@ -198,7 +213,7 @@ export class Agent {
private workspace: WorkspaceInfo | null = null;
private session: SessionInfo | null = null;
private messages: ModelMessage[] = [];
private recordedTokens = 0;
private messageSeqs: Array<number | null> = [];
private abortController: AbortController | null = null;
private maxToolRounds: number;
private mode: AgentMode = "agent";
@@ -230,8 +245,9 @@ export class Agent {
this.workspace = this.sessionStore.getWorkspace();
this.session = this.sessionStore.openSession(options.session, this.modelId, this.mode, this.bash.getCwd());
this.mode = this.session.mode;
this.messages = loadTranscript(this.session.id);
this.recordedTokens = getSessionTotalTokens(this.session.id);
const transcript = loadTranscriptState(this.session.id);
this.messages = transcript.messages;
this.messageSeqs = transcript.seqs;
this.sessionStore.setModel(this.session.id, this.modelId);
}
}
@@ -291,8 +307,7 @@ export class Agent {
ratioRemaining: number;
} {
const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext);
const estimatedTokens = estimateTokens(`${system}\n${JSON.stringify(this.messages)}\n${inFlightText}`);
const usedTokens = Math.min(contextWindow, Math.max(estimatedTokens, this.recordedTokens));
const usedTokens = Math.min(contextWindow, estimateConversationTokens(system, this.messages, inFlightText));
const remainingTokens = Math.max(0, contextWindow - usedTokens);
return {
@@ -331,7 +346,7 @@ export class Agent {
startNewSession(): SessionSnapshot | null {
if (!this.sessionStore) {
this.messages = [];
this.recordedTokens = 0;
this.messageSeqs = [];
return null;
}
@@ -339,7 +354,7 @@ export class Agent {
this.workspace = this.sessionStore.getWorkspace();
this.session = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
this.messages = [];
this.recordedTokens = 0;
this.messageSeqs = [];
return this.getSessionSnapshot();
}
@@ -388,6 +403,7 @@ export class Agent {
const idx = this.messages.lastIndexOf(userMessage);
if (idx >= 0) {
this.messages.splice(idx, 1);
this.messageSeqs.splice(idx, 1);
}
}
@@ -397,10 +413,6 @@ export class Agent {
model = this.modelId,
): void {
if (!usage) return;
const total = usage.totalTokens ?? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
if (Number.isFinite(total) && total > 0) {
this.recordedTokens += total;
}
if (this.session) {
recordUsageEvent(this.session.id, source, model, usage);
}
@@ -411,9 +423,11 @@ export class Agent {
const notifications = await this.delegations.consumeNotifications();
for (const notification of notifications) {
this.messages.push({ role: "system", content: notification.message });
let seq: number | null = null;
if (this.session) {
appendSystemMessage(this.session.id, notification.message);
seq = appendSystemMessage(this.session.id, notification.message);
}
this.messageSeqs.push(seq);
}
return notifications.map((notification) => notification.message);
} catch {
@@ -600,6 +614,62 @@ export class Agent {
}
}
private getCompactionSettings(): CompactionSettings {
return {
reserveTokens: Math.max(this.maxTokens, DEFAULT_RESERVE_TOKENS),
keepRecentTokens: DEFAULT_KEEP_RECENT_TOKENS,
};
}
private async compactForContext(
provider: XaiProvider,
system: string,
contextWindow: number,
signal: AbortSignal,
settings = this.getCompactionSettings(),
force = false,
): Promise<boolean> {
if (!this.session) return false;
const preparation = prepareCompaction(this.messages, system, settings);
if (!preparation) return false;
if (!force && !shouldCompactContext(preparation.tokensBefore, contextWindow, settings)) {
return false;
}
const keptSeqs = this.messageSeqs.slice(preparation.firstKeptIndex);
const firstKeptSeq = keptSeqs.find((seq): seq is number => seq !== null) ?? getNextMessageSequence(this.session.id);
const summary = await generateCompactionSummary(provider, this.modelId, preparation, undefined, signal);
appendCompaction(this.session.id, firstKeptSeq, summary, preparation.tokensBefore);
this.messages = [createCompactionSummaryMessage(summary), ...preparation.keptMessages];
this.messageSeqs = [null, ...keptSeqs];
return true;
}
private appendCompletedTurn(userMessage: ModelMessage, newMessages: ModelMessage[]): void {
if (newMessages.length === 0) return;
const userIndex = this.messages.lastIndexOf(userMessage);
if (!this.sessionStore || !this.session) {
if (userIndex >= 0 && this.messageSeqs[userIndex] == null) {
this.messageSeqs[userIndex] = null;
}
this.messages.push(...newMessages);
this.messageSeqs.push(...newMessages.map(() => null));
return;
}
const insertedSeqs = appendMessages(this.session.id, [userMessage, ...newMessages]);
if (userIndex >= 0) {
this.messageSeqs[userIndex] = insertedSeqs[0] ?? this.messageSeqs[userIndex];
}
this.messages.push(...newMessages);
this.messageSeqs.push(...insertedSeqs.slice(1));
this.sessionStore.touchSession(this.session.id, this.bash.getCwd());
this.session = this.sessionStore.getRequiredSession(this.session.id);
}
async *processMessage(userMessage: string): AsyncGenerator<StreamChunk, void, unknown> {
this.abortController = new AbortController();
const signal = this.abortController.signal;
@@ -608,147 +678,171 @@ export class Agent {
await this.consumeBackgroundNotifications();
const userModelMessage: ModelMessage = { role: "user", content: userMessage };
this.messages.push(userModelMessage);
this.messageSeqs.push(null);
let assistantText = "";
let streamOk = false;
let closeMcp: (() => Promise<void>) | undefined;
const provider = this.requireProvider();
const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext);
const modelInfo = getModelInfo(this.modelId);
this.planContext = null;
let attemptedOverflowRecovery = false;
try {
const provider = this.requireProvider();
const baseTools = createTools(this.bash, provider, this.mode, {
runTask: (request, abortSignal) => this.runTask(request, combineAbortSignals(signal, abortSignal)),
runDelegation: (request, abortSignal) => this.runDelegation(request, combineAbortSignals(signal, abortSignal)),
readDelegation: (id) => this.readDelegation(id),
listDelegations: () => this.listDelegations(),
});
let tools: ToolSet = baseTools;
if (this.mode === "agent") {
const mcpBundle = await buildMcpToolSet(loadMcpServers());
closeMcp = mcpBundle.close;
tools = { ...baseTools, ...mcpBundle.tools };
if (mcpBundle.errors.length > 0) {
yield { type: "content", content: `MCP unavailable: ${mcpBundle.errors.join(" | ")}\n\n` };
}
}
const system = buildSystemPrompt(this.bash.getCwd(), this.mode, this.planContext);
this.planContext = null;
while (true) {
let assistantText = "";
let streamOk = false;
let closeMcp: (() => Promise<void>) | undefined;
const result = streamText({
model: provider(this.modelId),
system,
messages: this.messages,
tools,
stopWhen: stepCountIs(this.maxToolRounds),
maxRetries: 0,
abortSignal: signal,
temperature: 0.7,
maxOutputTokens: this.maxTokens,
onFinish: ({ totalUsage }) => {
this.recordUsage(totalUsage, "message");
},
});
for await (const part of result.fullStream) {
if (signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
break;
}
switch (part.type) {
case "text-delta":
assistantText += part.text;
yield { type: "content", content: part.text };
break;
case "reasoning-delta":
yield { type: "reasoning", content: part.text };
break;
case "tool-call": {
const tc = toToolCall(part);
yield { type: "tool_calls", toolCalls: [tc] };
break;
try {
const settings = attemptedOverflowRecovery
? relaxCompactionSettings(this.getCompactionSettings())
: this.getCompactionSettings();
if (modelInfo) {
await this.compactForContext(
provider,
system,
modelInfo.contextWindow,
signal,
settings,
attemptedOverflowRecovery,
);
}
case "tool-result": {
const tc: ToolCall = {
id: part.toolCallId,
type: "function",
function: { name: part.toolName, arguments: JSON.stringify(part.input ?? {}) },
};
const tr = toToolResult(part.output);
yield { type: "tool_result", toolCall: tc, toolResult: tr };
break;
const baseTools = createTools(this.bash, provider, this.mode, {
runTask: (request, abortSignal) => this.runTask(request, combineAbortSignals(signal, abortSignal)),
runDelegation: (request, abortSignal) =>
this.runDelegation(request, combineAbortSignals(signal, abortSignal)),
readDelegation: (id) => this.readDelegation(id),
listDelegations: () => this.listDelegations(),
});
let tools: ToolSet = baseTools;
if (this.mode === "agent") {
const mcpBundle = await buildMcpToolSet(loadMcpServers());
closeMcp = mcpBundle.close;
tools = { ...baseTools, ...mcpBundle.tools };
if (mcpBundle.errors.length > 0) {
yield { type: "content", content: `MCP unavailable: ${mcpBundle.errors.join(" | ")}\n\n` };
}
}
case "error":
yield { type: "error", content: String(part.error) };
break;
const result = streamText({
model: provider(this.modelId),
system,
messages: this.messages,
tools,
stopWhen: stepCountIs(this.maxToolRounds),
maxRetries: 0,
abortSignal: signal,
temperature: 0.7,
maxOutputTokens: this.maxTokens,
onFinish: ({ totalUsage }) => {
this.recordUsage(totalUsage, "message");
},
});
case "abort":
for await (const part of result.fullStream) {
if (signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
break;
}
switch (part.type) {
case "text-delta":
assistantText += part.text;
yield { type: "content", content: part.text };
break;
case "reasoning-delta":
yield { type: "reasoning", content: part.text };
break;
case "tool-call": {
const tc = toToolCall(part);
yield { type: "tool_calls", toolCalls: [tc] };
break;
}
case "tool-result": {
const tc: ToolCall = {
id: part.toolCallId,
type: "function",
function: { name: part.toolName, arguments: JSON.stringify(part.input ?? {}) },
};
const tr = toToolResult(part.output);
yield { type: "tool_result", toolCall: tc, toolResult: tr };
break;
}
case "error":
yield { type: "error", content: String(part.error) };
break;
case "abort":
yield { type: "content", content: "\n\n[Cancelled]" };
break;
}
}
if (signal.aborted) {
this.discardAbortedTurn(userModelMessage);
yield { type: "done" };
return;
}
try {
const response = await result.response;
if (!signal.aborted) {
this.appendCompletedTurn(userModelMessage, response.messages);
streamOk = true;
}
} catch (responseError: unknown) {
if (
!attemptedOverflowRecovery &&
!assistantText.trim() &&
modelInfo &&
isContextLimitError(responseError)
) {
attemptedOverflowRecovery = true;
continue;
}
}
if (signal.aborted) {
this.discardAbortedTurn(userModelMessage);
yield { type: "done" };
return;
}
if (!streamOk && assistantText.trim()) {
this.appendCompletedTurn(userModelMessage, [{ role: "assistant", content: assistantText }]);
}
yield { type: "done" };
return;
} catch (err: unknown) {
if (signal.aborted) {
this.discardAbortedTurn(userModelMessage);
yield { type: "content", content: "\n\n[Cancelled]" };
break;
}
}
if (signal.aborted) {
this.discardAbortedTurn(userModelMessage);
yield { type: "done" };
return;
}
try {
const response = await result.response;
if (!signal.aborted) {
this.messages.push(...response.messages);
if (this.sessionStore && this.session) {
appendMessages(this.session.id, [userModelMessage, ...response.messages]);
this.sessionStore.touchSession(this.session.id, this.bash.getCwd());
this.session = this.sessionStore.getRequiredSession(this.session.id);
yield { type: "done" };
return;
}
streamOk = true;
}
} catch {
// response promise can fail after stream errors — fall back to manual message
}
if (signal.aborted) {
this.discardAbortedTurn(userModelMessage);
yield { type: "done" };
return;
}
if (!attemptedOverflowRecovery && !assistantText.trim() && modelInfo && isContextLimitError(err)) {
attemptedOverflowRecovery = true;
continue;
}
if (!streamOk && assistantText.trim()) {
const fallbackMessage: ModelMessage = { role: "assistant", content: assistantText };
this.messages.push(fallbackMessage);
if (this.sessionStore && this.session) {
appendMessages(this.session.id, [userModelMessage, fallbackMessage]);
this.sessionStore.touchSession(this.session.id, this.bash.getCwd());
this.session = this.sessionStore.getRequiredSession(this.session.id);
const msg = err instanceof Error ? err.message : String(err);
yield { type: "error", content: `Error: ${msg}` };
if (assistantText.trim()) {
this.appendCompletedTurn(userModelMessage, [{ role: "assistant", content: assistantText }]);
}
yield { type: "done" };
return;
} finally {
await closeMcp?.().catch(() => {});
}
}
yield { type: "done" };
} catch (err: unknown) {
if (signal.aborted) {
this.discardAbortedTurn(userModelMessage);
yield { type: "content", content: "\n\n[Cancelled]" };
} else {
const msg = err instanceof Error ? err.message : String(err);
yield { type: "error", content: `Error: ${msg}` };
}
if (!signal.aborted && assistantText.trim()) {
const fallbackMessage: ModelMessage = { role: "assistant", content: assistantText };
this.messages.push(fallbackMessage);
if (this.sessionStore && this.session) {
appendMessages(this.session.id, [userModelMessage, fallbackMessage]);
this.sessionStore.touchSession(this.session.id, this.bash.getCwd());
this.session = this.sessionStore.getRequiredSession(this.session.id);
}
}
yield { type: "done" };
} finally {
await closeMcp?.().catch(() => {});
if (this.abortController?.signal === signal) {
this.abortController = null;
}
@@ -828,10 +922,6 @@ function truncate(text: string, max: number): string {
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
if (activeSignals.length === 0) return undefined;
@@ -853,3 +943,8 @@ function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortS
return controller.signal;
}
function isContextLimitError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return /(context|token|prompt).*(limit|length|large|window|overflow)|too many tokens|maximum context/i.test(message);
}
+134
View File
@@ -0,0 +1,134 @@
import type { ModelMessage } from "ai";
import { describe, expect, it } from "vitest";
import { buildEffectiveTranscript, type PersistedCompaction } from "../storage/transcript-view";
import {
COMPACTION_SUMMARY_HEADER,
createCompactionSummaryMessage,
findCutPoint,
prepareCompaction,
serializeConversation,
shouldCompactContext,
} from "./compaction";
function user(text: string): ModelMessage {
return { role: "user", content: text } as ModelMessage;
}
function assistantText(text: string): ModelMessage {
return {
role: "assistant",
content: [{ type: "text", text }],
} as ModelMessage;
}
function assistantToolCall(toolCallId: string, toolName: string, input: Record<string, unknown> = {}): ModelMessage {
return {
role: "assistant",
content: [{ type: "tool-call", toolCallId, toolName, input }],
} as ModelMessage;
}
function toolResult(toolCallId: string, toolName: string, output: unknown): ModelMessage {
return {
role: "tool",
content: [{ type: "tool-result", toolCallId, toolName, output }],
} as ModelMessage;
}
describe("compaction helpers", () => {
it("triggers when context exceeds reserved headroom", () => {
const settings = { reserveTokens: 100, keepRecentTokens: 40 };
expect(shouldCompactContext(901, 1000, settings)).toBe(true);
expect(shouldCompactContext(900, 1000, settings)).toBe(false);
});
it("never selects a tool-result message as the cut point", () => {
const messages = [
user("inspect this file"),
assistantToolCall("call-1", "read_file", { path: "src/index.ts" }),
toolResult("call-1", "read_file", "x".repeat(400)),
assistantText("I found the relevant section."),
user("continue"),
];
const cutPoint = findCutPoint(messages, 0, 130);
expect(cutPoint.firstKeptIndex).not.toBe(2);
expect(messages[cutPoint.firstKeptIndex]?.role).not.toBe("tool");
});
it("detects split turns and captures the turn prefix for summarization", () => {
const messages = [
user("Refactor the session loader."),
assistantText("a".repeat(320)),
assistantText("recent status update"),
];
const preparation = prepareCompaction(messages, "system prompt", {
reserveTokens: 100,
keepRecentTokens: 5,
});
expect(preparation).not.toBeNull();
expect(preparation?.isSplitTurn).toBe(true);
expect(preparation?.messagesToSummarize).toHaveLength(0);
expect(preparation?.turnPrefixMessages).toHaveLength(2);
expect(preparation?.keptMessages).toHaveLength(1);
});
it("excludes the previous summary message from new compaction input", () => {
const messages = [
createCompactionSummaryMessage("Earlier work"),
user("Handle migration edge cases"),
assistantText("I added the table and loader changes."),
user("Now wire the retry path"),
];
const preparation = prepareCompaction(messages, "system prompt", {
reserveTokens: 100,
keepRecentTokens: 4,
});
expect(preparation).not.toBeNull();
expect(preparation?.previousSummary).toBe("Earlier work");
expect(preparation?.messagesToSummarize[0]).toEqual(user("Handle migration edge cases"));
expect(preparation?.messagesToSummarize.some((message) => message.role === "system")).toBe(false);
});
it("serializes tool results with truncation markers", () => {
const transcript = serializeConversation([user("Read the output"), toolResult("call-1", "bash", "x".repeat(2105))]);
expect(transcript).toContain("[Tool result]:");
expect(transcript).toContain("more characters truncated");
});
it("builds the effective transcript from the latest persisted checkpoint", () => {
const rawMessages = [
user("old request"),
assistantText("old answer"),
user("new request"),
assistantText("new answer"),
user("latest request"),
];
const seqs = [1, 2, 3, 4, 5];
const timestamps = seqs.map((seq) => new Date(`2026-03-20T00:00:0${seq}.000Z`));
const checkpoint: PersistedCompaction = {
firstKeptSeq: 4,
summary: "Summarized old work",
tokensBefore: 1234,
createdAt: new Date("2026-03-20T00:00:10.000Z"),
};
const transcript = buildEffectiveTranscript(rawMessages, seqs, timestamps, checkpoint);
expect(transcript.messages).toHaveLength(3);
expect(transcript.seqs).toEqual([null, 4, 5]);
expect(transcript.messages[0]).toEqual(createCompactionSummaryMessage("Summarized old work"));
expect(
transcript.messages[0]?.role === "system" && typeof transcript.messages[0].content === "string"
? transcript.messages[0].content
: "",
).toContain(COMPACTION_SUMMARY_HEADER);
});
});
+476
View File
@@ -0,0 +1,476 @@
import { generateText, type ModelMessage } from "ai";
import type { XaiProvider } from "../grok/client";
export interface CompactionSettings {
reserveTokens: number;
keepRecentTokens: number;
}
export interface CutPointResult {
firstKeptIndex: number;
turnStartIndex: number;
isSplitTurn: boolean;
}
export interface PreparedCompaction {
previousSummary?: string;
messagesToSummarize: ModelMessage[];
turnPrefixMessages: ModelMessage[];
keptMessages: ModelMessage[];
firstKeptIndex: number;
isSplitTurn: boolean;
tokensBefore: number;
settings: CompactionSettings;
}
const TOOL_RESULT_MAX_CHARS = 2000;
const MIN_KEPT_TOKENS_ON_RETRY = 4000;
export const DEFAULT_RESERVE_TOKENS = 16_384;
export const DEFAULT_KEEP_RECENT_TOKENS = 20_000;
export const COMPACTION_SUMMARY_HEADER = "[Context checkpoint summary]";
const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant.
Do not continue the conversation. Do not answer any questions from the conversation.
Only output a structured checkpoint summary that another coding agent can use to continue the work.`;
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
Use this exact format:
## Goal
[What the user is trying to accomplish]
## Constraints & Preferences
- [Requirements, preferences, or constraints]
- [(none) if none were mentioned]
## Progress
### Done
- [x] [Completed work]
### In Progress
- [ ] [Current work]
### Blocked
- [Any active blockers]
## Key Decisions
- **[Decision]**: [Rationale]
## Next Steps
1. [The next action to take]
## Critical Context
- [Important details needed to continue]
- [(none) if not applicable]
Keep it concise, but preserve exact file paths, function names, and error messages.`;
const UPDATE_SUMMARIZATION_PROMPT = `The messages above are new conversation messages to incorporate into the existing summary provided below.
Update the existing structured summary with new information. Rules:
- Preserve still-relevant information from the previous summary
- Add new progress, decisions, and critical context
- Move completed items from "In Progress" to "Done" when appropriate
- Update "Next Steps" based on the current state
- Preserve exact file paths, function names, and error messages
Use the exact same section structure as the existing summary format.`;
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the early prefix of a single turn that was too large to keep in full. The recent suffix is still available.
Summarize only what is needed so another coding agent can understand the retained suffix.
Use this exact format:
## Original Request
[What the user asked for in this turn]
## Early Progress
- [Key work done before the kept suffix]
## Context For Suffix
- [Information needed to understand the kept recent messages]`;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function getTextParts(content: unknown): string[] {
if (typeof content === "string") return [content];
if (!Array.isArray(content)) return [];
const parts: string[] = [];
for (const part of content) {
if (!isRecord(part)) continue;
if (part.type === "text" && typeof part.text === "string") {
parts.push(part.text);
continue;
}
if (part.type === "reasoning" && typeof part.text === "string") {
parts.push(part.text);
continue;
}
if (part.type === "reasoning" && typeof part.reasoning === "string") {
parts.push(part.reasoning);
}
}
return parts;
}
function stringifyForSummary(value: unknown): string {
if (typeof value === "string") return value;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function truncateForSummary(text: string, maxChars = TOOL_RESULT_MAX_CHARS): string {
if (text.length <= maxChars) return text;
return `${text.slice(0, maxChars)}\n\n[... ${text.length - maxChars} more characters truncated]`;
}
function extractUserContent(content: unknown): string {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
const parts: string[] = [];
for (const part of content) {
if (!isRecord(part)) continue;
if (part.type === "text" && typeof part.text === "string") {
parts.push(part.text);
continue;
}
if (part.type === "image") {
parts.push("[Image]");
continue;
}
if (part.type === "file") {
const filename = typeof part.filename === "string" ? part.filename : null;
parts.push(filename ? `[File: ${filename}]` : "[File]");
}
}
return parts.join("\n");
}
function extractAssistantText(content: unknown): string {
return getTextParts(content).join("\n");
}
function extractToolCallText(content: unknown): string[] {
if (!Array.isArray(content)) return [];
const toolCalls: string[] = [];
for (const part of content) {
if (!isRecord(part) || part.type !== "tool-call") continue;
const toolName = typeof part.toolName === "string" ? part.toolName : "tool";
const input = isRecord(part.input) ? part.input : {};
const args = Object.entries(input)
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join(", ");
toolCalls.push(`${toolName}(${args})`);
}
return toolCalls;
}
function extractToolResultText(content: unknown): string[] {
if (!Array.isArray(content)) return [];
const toolResults: string[] = [];
for (const part of content) {
if (!isRecord(part) || part.type !== "tool-result") continue;
toolResults.push(truncateForSummary(stringifyForSummary(part.output)));
}
return toolResults;
}
export function createCompactionSummaryMessage(summary: string): ModelMessage {
return {
role: "system",
content: `${COMPACTION_SUMMARY_HEADER}\n${summary.trim()}`,
};
}
export function isCompactionSummaryMessage(message: ModelMessage | undefined): boolean {
return message?.role === "system" && typeof message.content === "string"
? message.content.startsWith(COMPACTION_SUMMARY_HEADER)
: false;
}
export function getCompactionSummaryText(message: ModelMessage | undefined): string | null {
if (!isCompactionSummaryMessage(message) || typeof message?.content !== "string") {
return null;
}
return message.content.slice(COMPACTION_SUMMARY_HEADER.length).trim();
}
export function estimateMessageTokens(message: ModelMessage): number {
let chars = 0;
switch (message.role) {
case "user":
chars += extractUserContent(message.content).length;
break;
case "assistant":
chars += extractAssistantText(message.content).length;
chars += extractToolCallText(message.content).join("; ").length;
break;
case "tool":
chars += extractToolResultText(message.content).join("\n").length;
break;
case "system":
chars +=
typeof message.content === "string" ? message.content.length : getTextParts(message.content).join("\n").length;
break;
default:
chars += stringifyForSummary((message as { content?: unknown }).content).length;
break;
}
return Math.ceil(chars / 4);
}
export function estimateConversationTokens(systemPrompt: string, messages: ModelMessage[], inFlightText = ""): number {
const systemTokens = Math.ceil((systemPrompt.length + inFlightText.length) / 4);
return systemTokens + messages.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
}
export function shouldCompactContext(
contextTokens: number,
contextWindow: number,
settings: CompactionSettings,
): boolean {
return contextTokens > contextWindow - settings.reserveTokens;
}
function isValidCutPoint(message: ModelMessage): boolean {
return message.role !== "tool";
}
function findTurnStartIndex(messages: ModelMessage[], entryIndex: number, startIndex: number): number {
for (let i = entryIndex; i >= startIndex; i--) {
if (messages[i]?.role === "user") {
return i;
}
}
return -1;
}
export function findCutPoint(messages: ModelMessage[], startIndex: number, keepRecentTokens: number): CutPointResult {
const cutPoints: number[] = [];
for (let i = startIndex; i < messages.length; i++) {
if (isValidCutPoint(messages[i])) {
cutPoints.push(i);
}
}
if (cutPoints.length === 0) {
return { firstKeptIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
}
let accumulatedTokens = 0;
let cutIndex = cutPoints[0];
for (let i = messages.length - 1; i >= startIndex; i--) {
accumulatedTokens += estimateMessageTokens(messages[i]);
if (accumulatedTokens >= keepRecentTokens) {
cutIndex = cutPoints.find((index) => index >= i) ?? cutPoints[cutPoints.length - 1];
break;
}
}
const cutMessage = messages[cutIndex];
const isUserMessage = cutMessage?.role === "user";
const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(messages, cutIndex, startIndex);
return {
firstKeptIndex: cutIndex,
turnStartIndex,
isSplitTurn: !isUserMessage && turnStartIndex !== -1,
};
}
export function prepareCompaction(
messages: ModelMessage[],
systemPrompt: string,
settings: CompactionSettings,
): PreparedCompaction | null {
const previousSummary = getCompactionSummaryText(messages[0]) ?? undefined;
const boundaryStart = previousSummary ? 1 : 0;
if (boundaryStart >= messages.length) {
return null;
}
const cutPoint = findCutPoint(messages, boundaryStart, settings.keepRecentTokens);
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptIndex;
const messagesToSummarize = messages.slice(boundaryStart, Math.max(boundaryStart, historyEnd));
const turnPrefixMessages = cutPoint.isSplitTurn
? messages.slice(cutPoint.turnStartIndex, cutPoint.firstKeptIndex)
: [];
const keptMessages = messages.slice(cutPoint.firstKeptIndex);
const tokensBefore = estimateConversationTokens(systemPrompt, messages);
if (keptMessages.length === 0) {
return null;
}
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
return null;
}
return {
previousSummary,
messagesToSummarize,
turnPrefixMessages,
keptMessages,
firstKeptIndex: cutPoint.firstKeptIndex,
isSplitTurn: cutPoint.isSplitTurn,
tokensBefore,
settings,
};
}
export function relaxCompactionSettings(settings: CompactionSettings): CompactionSettings {
return {
...settings,
keepRecentTokens: Math.max(MIN_KEPT_TOKENS_ON_RETRY, Math.floor(settings.keepRecentTokens / 2)),
};
}
export function serializeConversation(messages: ModelMessage[]): string {
const parts: string[] = [];
for (const message of messages) {
if (isCompactionSummaryMessage(message)) {
const summary = getCompactionSummaryText(message);
if (summary) {
parts.push(`[Previous summary]: ${summary}`);
}
continue;
}
if (message.role === "user") {
const content = extractUserContent(message.content).trim();
if (content) parts.push(`[User]: ${content}`);
continue;
}
if (message.role === "assistant") {
const text = extractAssistantText(message.content).trim();
const toolCalls = extractToolCallText(message.content);
if (text) parts.push(`[Assistant]: ${text}`);
if (toolCalls.length > 0) parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
continue;
}
if (message.role === "tool") {
const results = extractToolResultText(message.content);
for (const result of results) {
if (result.trim()) parts.push(`[Tool result]: ${result}`);
}
continue;
}
if (message.role === "system") {
const content =
typeof message.content === "string" ? message.content.trim() : getTextParts(message.content).join("\n").trim();
if (content) parts.push(`[System]: ${content}`);
}
}
return parts.join("\n\n");
}
async function summarizeConversation(
provider: XaiProvider,
modelId: string,
messages: ModelMessage[],
reserveTokens: number,
customInstructions?: string,
previousSummary?: string,
promptOverride?: string,
signal?: AbortSignal,
): Promise<string> {
const serialized = serializeConversation(messages);
const promptParts = [serialized];
if (previousSummary) {
promptParts.push(`Existing summary:\n${previousSummary}`);
}
const basePrompt = promptOverride ?? (previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT);
promptParts.push(basePrompt);
if (customInstructions?.trim()) {
promptParts.push(`Additional focus: ${customInstructions.trim()}`);
}
const { text } = await generateText({
model: provider(modelId),
system: SUMMARIZATION_SYSTEM_PROMPT,
prompt: promptParts.filter(Boolean).join("\n\n"),
abortSignal: signal,
maxRetries: 0,
temperature: 0.2,
maxOutputTokens: Math.max(512, Math.floor(reserveTokens * 0.8)),
});
return text.trim();
}
export async function generateCompactionSummary(
provider: XaiProvider,
modelId: string,
preparation: PreparedCompaction,
customInstructions?: string,
signal?: AbortSignal,
): Promise<string> {
const { messagesToSummarize, turnPrefixMessages, isSplitTurn, previousSummary, settings } = preparation;
if (isSplitTurn && turnPrefixMessages.length > 0) {
const [historySummary, prefixSummary] = await Promise.all([
messagesToSummarize.length > 0
? summarizeConversation(
provider,
modelId,
messagesToSummarize,
settings.reserveTokens,
customInstructions,
previousSummary,
undefined,
signal,
)
: Promise.resolve(previousSummary?.trim() || ""),
summarizeConversation(
provider,
modelId,
turnPrefixMessages,
settings.reserveTokens,
undefined,
undefined,
TURN_PREFIX_SUMMARIZATION_PROMPT,
signal,
),
]);
if (historySummary && prefixSummary) {
return `${historySummary}\n\n---\n\n${prefixSummary}`;
}
return (historySummary || prefixSummary).trim();
}
return summarizeConversation(
provider,
modelId,
messagesToSummarize,
settings.reserveTokens,
customInstructions,
previousSummary,
undefined,
signal,
);
}
+12 -1
View File
@@ -1,4 +1,15 @@
export { getDatabasePath } from "./db";
export { SessionStore } from "./sessions";
export { appendMessages, appendSystemMessage, buildChatEntries, loadTranscript } from "./transcript";
export {
appendCompaction,
appendMessages,
appendSystemMessage,
buildChatEntries,
getNextMessageSequence,
loadLatestCompaction,
loadRawTranscript,
loadTranscript,
loadTranscriptState,
} from "./transcript";
export { buildEffectiveTranscript, type LoadedTranscriptState, type PersistedCompaction } from "./transcript-view";
export { getSessionTotalTokens, listSessionUsage, recordUsageEvent, type TokenUsageLike } from "./usage";
+32 -1
View File
@@ -1,6 +1,6 @@
import type { SQLiteDatabase } from "./db";
const LATEST_DB_VERSION = 1;
const LATEST_DB_VERSION = 2;
export function applyMigrations(db: SQLiteDatabase): void {
const version = Number(db.pragma("user_version", { simple: true })) || 0;
@@ -11,6 +11,10 @@ export function applyMigrations(db: SQLiteDatabase): void {
createInitialSchema(db);
db.pragma("user_version = 1");
}
if (version < 2) {
createCompactionSchema(db);
db.pragma("user_version = 2");
}
});
migrate();
@@ -84,6 +88,15 @@ function createInitialSchema(db: SQLiteDatabase): void {
created_at TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS compactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
first_kept_seq INTEGER NOT NULL,
summary TEXT NOT NULL,
tokens_before INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
) STRICT;
CREATE INDEX IF NOT EXISTS idx_sessions_workspace_updated
ON sessions(workspace_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_messages_session_seq
@@ -92,5 +105,23 @@ function createInitialSchema(db: SQLiteDatabase): void {
ON tool_calls(session_id, message_seq);
CREATE INDEX IF NOT EXISTS idx_usage_events_session_created
ON usage_events(session_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_compactions_session_created
ON compactions(session_id, created_at DESC);
`);
}
function createCompactionSchema(db: SQLiteDatabase): void {
db.exec(`
CREATE TABLE IF NOT EXISTS compactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
first_kept_seq INTEGER NOT NULL,
summary TEXT NOT NULL,
tokens_before INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
) STRICT;
CREATE INDEX IF NOT EXISTS idx_compactions_session_created
ON compactions(session_id, created_at DESC);
`);
}
+45
View File
@@ -0,0 +1,45 @@
import type { ModelMessage } from "ai";
import { createCompactionSummaryMessage } from "../agent/compaction";
export interface PersistedCompaction {
firstKeptSeq: number;
summary: string;
tokensBefore: number;
createdAt: Date;
}
export interface LoadedTranscriptState {
messages: ModelMessage[];
seqs: Array<number | null>;
timestamps: Date[];
compaction: PersistedCompaction | null;
}
export function buildEffectiveTranscript(
messages: ModelMessage[],
seqs: number[],
timestamps: Date[],
compaction: PersistedCompaction | null,
): LoadedTranscriptState {
if (!compaction) {
return {
messages: [...messages],
seqs: [...seqs],
timestamps: [...timestamps],
compaction: null,
};
}
const firstKeptIndex = seqs.findIndex((seq) => seq >= compaction.firstKeptSeq);
const keptIndex = firstKeptIndex >= 0 ? firstKeptIndex : messages.length;
const keptMessages = messages.slice(keptIndex);
const keptSeqs = seqs.slice(keptIndex);
const keptTimestamps = timestamps.slice(keptIndex);
return {
messages: [createCompactionSummaryMessage(compaction.summary), ...keptMessages],
seqs: [null, ...keptSeqs],
timestamps: [compaction.createdAt, ...keptTimestamps],
compaction,
};
}
+105 -21
View File
@@ -1,6 +1,8 @@
import type { ModelMessage } from "ai";
import { getCompactionSummaryText } from "../agent/compaction";
import type { ChatEntry, ToolCall, ToolResult } from "../types/index";
import { getDatabase, withTransaction } from "./db";
import { buildEffectiveTranscript, type LoadedTranscriptState, type PersistedCompaction } from "./transcript-view";
interface MessageRow {
session_id: string;
@@ -22,8 +24,22 @@ interface StoredToolResultRow {
output_json: string;
}
export function loadTranscript(sessionId: string): ModelMessage[] {
const rows = getDatabase()
interface CompactionRow {
session_id: string;
first_kept_seq: number;
summary: string;
tokens_before: number;
created_at: string;
}
interface EffectiveMessageRecord {
message: ModelMessage;
seq: number | null;
timestamp: Date;
}
function loadMessageRows(sessionId: string): MessageRow[] {
return getDatabase()
.prepare(`
SELECT session_id, seq, role, message_json, created_at
FROM messages
@@ -31,13 +47,72 @@ export function loadTranscript(sessionId: string): ModelMessage[] {
ORDER BY seq ASC
`)
.all(sessionId) as MessageRow[];
return rows.map((row) => JSON.parse(row.message_json) as ModelMessage);
}
export function appendMessages(sessionId: string, messages: ModelMessage[]): void {
if (messages.length === 0) return;
function toPersistedCompaction(row: CompactionRow | undefined): PersistedCompaction | null {
if (!row) return null;
return {
firstKeptSeq: row.first_kept_seq,
summary: row.summary,
tokensBefore: row.tokens_before,
createdAt: new Date(row.created_at),
};
}
export function loadLatestCompaction(sessionId: string): PersistedCompaction | null {
const row = getDatabase()
.prepare(`
SELECT session_id, first_kept_seq, summary, tokens_before, created_at
FROM compactions
WHERE session_id = ?
ORDER BY id DESC
LIMIT 1
`)
.get(sessionId) as CompactionRow | undefined;
return toPersistedCompaction(row);
}
function buildEffectiveMessageRecords(sessionId: string): EffectiveMessageRecord[] {
const rows = loadMessageRows(sessionId);
const messages = rows.map((row) => JSON.parse(row.message_json) as ModelMessage);
const seqs = rows.map((row) => row.seq);
const timestamps = rows.map((row) => new Date(row.created_at));
const transcript = buildEffectiveTranscript(messages, seqs, timestamps, loadLatestCompaction(sessionId));
return transcript.messages.map((message, index) => ({
message,
seq: transcript.seqs[index],
timestamp: transcript.timestamps[index],
}));
}
export function loadRawTranscript(sessionId: string): ModelMessage[] {
return loadMessageRows(sessionId).map((row) => JSON.parse(row.message_json) as ModelMessage);
}
export function loadTranscriptState(sessionId: string): LoadedTranscriptState {
const rows = loadMessageRows(sessionId);
return buildEffectiveTranscript(
rows.map((row) => JSON.parse(row.message_json) as ModelMessage),
rows.map((row) => row.seq),
rows.map((row) => new Date(row.created_at)),
loadLatestCompaction(sessionId),
);
}
export function loadTranscript(sessionId: string): ModelMessage[] {
return loadTranscriptState(sessionId).messages;
}
export function getNextMessageSequence(sessionId: string): number {
return getNextSequence(getDatabase(), sessionId);
}
export function appendMessages(sessionId: string, messages: ModelMessage[]): number[] {
if (messages.length === 0) return [];
const insertedSeqs: number[] = [];
withTransaction((db) => {
const nextSeq = getNextSequence(db, sessionId);
const insertMessage = db.prepare(`
@@ -72,6 +147,7 @@ export function appendMessages(sessionId: string, messages: ModelMessage[]): voi
messages.forEach((message, index) => {
const seq = nextSeq + index;
const createdAt = new Date().toISOString();
insertedSeqs.push(seq);
insertMessage.run(sessionId, seq, message.role, JSON.stringify(message), createdAt);
if (message.role === "assistant" && Array.isArray(message.content)) {
@@ -122,29 +198,36 @@ export function appendMessages(sessionId: string, messages: ModelMessage[]): voi
updateSession.run(new Date().toISOString(), sessionId);
});
return insertedSeqs;
}
export function appendSystemMessage(sessionId: string, content: string): void {
appendMessages(sessionId, [{ role: "system", content }]);
export function appendSystemMessage(sessionId: string, content: string): number | null {
return appendMessages(sessionId, [{ role: "system", content }])[0] ?? null;
}
export function appendCompaction(sessionId: string, firstKeptSeq: number, summary: string, tokensBefore: number): void {
withTransaction((db) => {
db.prepare(`
INSERT INTO compactions (session_id, first_kept_seq, summary, tokens_before, created_at)
VALUES (?, ?, ?, ?, ?)
`).run(sessionId, firstKeptSeq, summary, tokensBefore, new Date().toISOString());
db.prepare(`
UPDATE sessions
SET updated_at = ?
WHERE id = ?
`).run(new Date().toISOString(), sessionId);
});
}
export function buildChatEntries(sessionId: string): ChatEntry[] {
const db = getDatabase();
const messageRows = db
.prepare(`
SELECT session_id, seq, role, message_json, created_at
FROM messages
WHERE session_id = ?
ORDER BY seq ASC
`)
.all(sessionId) as MessageRow[];
const toolResults = loadStoredToolResults(sessionId);
const callMap = new Map<string, ToolCall>();
const entries: ChatEntry[] = [];
for (const row of messageRows) {
const message = JSON.parse(row.message_json) as ModelMessage;
const timestamp = new Date(row.created_at);
for (const row of buildEffectiveMessageRecords(sessionId)) {
const { message, timestamp } = row;
if (message.role === "user") {
const content = renderUserContent(message.content);
@@ -155,7 +238,8 @@ export function buildChatEntries(sessionId: string): ChatEntry[] {
}
if (message.role === "system") {
const content = typeof message.content === "string" ? message.content.trim() : "";
const content =
getCompactionSummaryText(message) ?? (typeof message.content === "string" ? message.content.trim() : "");
if (content) {
entries.push({ type: "assistant", content, timestamp });
}
+6
View File
@@ -1177,6 +1177,12 @@ export function App({ agent, startupConfig, initialMessage, onExit }: AppProps)
]);
}
if (!isStale() && agent.getSessionId()) {
setMessages(agent.getChatEntries());
setSessionTitle(agent.getSessionTitle());
setSessionId(agent.getSessionId());
}
contentAccRef.current = "";
if (!isStale()) {
setStreamContent("");