mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-10 16:23:44 +00:00
* ⚡ feat: Persist HITL checkpoints only on pause (skip clean-exit writes) With `durability: 'exit'` (set by the SDK whenever a checkpointer is active) LangGraph persists ONE checkpoint at the exit boundary on EVERY run — paused or not. So a non-paused HITL turn writes a dead checkpoint whose only fate is to be pruned by deleteAgentCheckpoint: pure write+delete churn on the common path, given HITL only ever resumes an *interrupt* checkpoint. `InterruptOnlyMongoSaver` (a MongoDBSaver subclass) persists only interrupt checkpoints and discards clean-exit ones, so a non-paused turn writes nothing. How it tells them apart (verified empirically against @langchain/langgraph, not docs): when a run interrupts, the runner calls `putWrites` with the `INTERRUPT` ("__interrupt__") channel for the checkpoint it's about to create, and that write's `config.checkpoint_id` equals the `checkpoint.id` of the `put` that immediately follows. A clean exit calls `put` with no preceding interrupt `putWrites`. So we record the checkpoint id of any interrupt `putWrites` and persist a `put` only when its `checkpoint.id` was so marked. Keying on the globally-unique checkpoint id (not thread_id) keeps this correct even when two runs race on the same conversation (the job-replacement scenario). Correctness is preserved end-to-end: interrupt checkpoints + their pending writes persist exactly as before (resume unchanged); clean checkpoints were only ever written-then-pruned, so not writing them is observationally equivalent. The eager prune stays as the backstop. Tests (mongodb-memory-server): a bare put() is discarded; an interrupt-seeded checkpoint is persisted with its __interrupt__ pending write; and an end-to-end real-graph run writes 0 checkpoints on a clean completion and a resumable one on interrupt. NOTE: a non-paused turn's deleteAgentCheckpoint now finds nothing to delete (a 0-match no-op) — a follow-up can skip that call entirely once the lingering-abandoned-pause cleanup role is reassigned to the TTL + expiry sweeper. * ⚡ feat: Drop the redundant clean-path checkpoint prune With the lazy checkpointer (InterruptOnlyMongoSaver) a non-paused turn no longer writes a clean-exit checkpoint, so the post-completion prune in chatCompletion's finally had nothing left to delete. It was also already redundant: every fresh turn runs a pre-run prune (`deleteAgentCheckpoint` before `processStream`) that clears any checkpoint orphaned by a prior abandoned pause — verified empirically that a lingering interrupt checkpoint WOULD otherwise poison a fresh turn (LangGraph continues the abandoned state + re-interrupts), and that the pre-run prune is what prevents it. The Mongo TTL remains the backstop, and the resume path still prunes after a successful finalize. Removing the clean-path prune also deletes its job-replacement race surface (round-17 F21): an older run's late finally can no longer delete a newer paused run's checkpoint, because there is no longer a clean-path prune to race. Dropped the now-dead F21 predicate test. Net per non-paused HITL turn: from {pre-run prune + checkpoint write + post-run prune} down to {pre-run prune} — no write, no post-completion delete. * 🛡️ fix: Anchor any pending-write checkpoint; stale-only eviction (Codex) Broaden the lazy saver's keep-rule from "interrupt-only" to "persist any checkpoint that carries pending writes" (renamed InterruptOnlyMongoSaver → LazyMongoSaver). This makes it robust to delta-channel graphs without changing behavior for LibreChat's graph: - K1 (P1): a delta-channel graph can write a synthetic PARENT/anchor checkpoint (no __interrupt__ mark) that the interrupt checkpoint then points at, with the delta writes stored under the parent id. The old rule discarded that parent, breaking delta-state resume. Now any checkpoint that received putWrites is persisted, so the anchor parent and its writes survive and resume can walk the chain. - K3 (P2): for the same reason, clean delta-write rows are no longer orphaned — their checkpoint is persisted alongside them. (For LibreChat's standard Annotation/messages graph a clean run makes no putWrites at all — verified empirically — so the common path still writes nothing and the optimization is unchanged.) - K2 (P2): the 1024 FIFO cap could evict a valid in-flight id whose put() was just behind Mongo I/O, mis-classifying its interrupt checkpoint as a clean exit. Replaced with time-based eviction: only ids older than 5 min (a put always follows its putWrites within ms) are swept; a recent in-flight id is never dropped, and the map grows rather than evict a valid id if nothing is stale. New integration test: a checkpoint anchored by a NON-interrupt write is persisted. Full agents/HITL suites green (108). * style(checkpointer): fix import order to satisfy sort-imports CI * fix(checkpointer): don't persist failed-turn (error-only) checkpoints LazyMongoSaver anchored on ANY pending write, so a non-paused turn that errors (LangGraph records an __error__ write then a put) was persisted and, with the clean-path prune removed, lingered until the next fresh-turn prune or the Mongo TTL. Anchor only on resumable writes — INTERRUPT or a real (non-__-prefixed) state/delta channel — so error/bookkeeping-only checkpoints are discarded at the source. Addresses Codex P3. Codex P2 (delta-stub parent orphan) is not reachable: the SDK graph uses standard Annotation/MessagesAnnotation channels (no DeltaChannel), and under durability:'exit' putWrites precedes put with a parentless boundary checkpoint — probe-confirmed against @langchain/langgraph@1.4. Documented the durability:'exit' invariant the saver depends on. Tests: error-only put discarded; e2e throwing graph persists 0 checkpoints. * fix(checkpointer): drop bookkeeping-only write batches, not just the checkpoint The prior fix stopped the failed-turn CHECKPOINT from persisting, but putWrites still forwarded the __error__ batch to MongoDBSaver.putWrites — writing a row to agent_checkpoint_writes whose parent checkpoint is then discarded. With the post-run deleteThread removed, that orphan row lingered until the Mongo TTL or the conversation's next pre-run prune. putWrites now drops a non-resumable (bookkeeping-only) batch entirely instead of forwarding it. Probed against a real MongoDBSaver (mongodb-memory-server): a throwing graph now leaves 0 checkpoints AND 0 write rows (was 0 + 1 orphan), while interrupt->resume is unaffected — the __interrupt__ write is resumable so it is still forwarded. Addresses Codex P2 (round 3). Tests: error-only put leaves no checkpoint and no write row; e2e throwing graph leaves both collections empty; new e2e interrupt->resume completes with the approval value. * fix(checkpointer): un-anchor a checkpoint whose putWrites failed; freshen comments Self-review findings on the converged PR: 1. LangGraph dispatches put() concurrently with putWrites (probe-confirmed on 1.4.5), and put() still completes when putWrites rejects — so a transient Mongo failure during the interrupt write could persist a checkpoint whose __interrupt__ row is missing (an unresumable phantom pause). putWrites now deletes the write anchor on rejection (best-effort) and rethrows, so that put() discards the checkpoint instead. The pre-recorded anchor stays where it is — recording after the await would drop slow-I/O interrupts on the success path, which the same probe showed is reachable. 2. Renamed leftovers: two comments still said InterruptOnlyMongoSaver; the class is LazyMongoSaver. 3. Documented why the pre-run prune is deliberately unconditional per HITL turn (any cheaper gate can go stale across replicas and skip the prune exactly when an orphaned interrupt exists). Test: failed putWrites → subsequent put persists nothing (14/14 green). * fix(checkpointer): bookkeeping write batches follow their checkpoint's fate The round-3 rule dropped bookkeeping-only putWrites batches (__error__/ __resume__/__no_writes__) unconditionally — batch-scoped, when the decision must be checkpoint-scoped. Probe-confirmed (langgraph 1.4.5, durability:'exit'): a Send fan-out that pauses on one sibling records the completed siblings as pure __no_writes__ batches on the RETAINED interrupt checkpoint; dropping those markers makes resume re-execute the completed siblings (side effects measured twice). Addresses Codex M2 (P2). putWrites now PARKS a bookkeeping-only batch in memory until the checkpoint's fate is known: forwarded when the checkpoint is anchored (or was just persisted — put is dispatched concurrently), dropped when put discards it. Net: an errored turn still leaves nothing durable (0 checkpoints, 0 write rows), and a retained checkpoint stores byte-for-byte what a plain MongoDBSaver would. Codex M1 (__resume__ lost on re-pause) did not reproduce: the re-pause emits [__interrupt__,__resume__] as ONE batch (anchored, forwarded whole) and a second resume on a rebuilt graph replays both answers correctly — but the fate-scoped buffering now covers a lone __resume__ batch in any ordering too. Tests: bookkeeping preserved on a retained checkpoint in either arrival order; e2e Send-sibling pause/resume with side-effect counters (was {a:2,c:2} under the drop rule, now {a:1,c:1}); error-only turn still leaves both collections empty. 16/16 green.
172 lines
6.6 KiB
JSON
172 lines
6.6 KiB
JSON
{
|
|
"name": "@librechat/api",
|
|
"version": "1.7.34",
|
|
"type": "commonjs",
|
|
"description": "MCP services for LibreChat",
|
|
"main": "dist/index.cjs",
|
|
"types": "./dist/index.d.cts",
|
|
"exports": {
|
|
".": {
|
|
"require": {
|
|
"types": "./dist/index.d.cts",
|
|
"default": "./dist/index.cjs"
|
|
}
|
|
},
|
|
"./telemetry": {
|
|
"require": {
|
|
"types": "./dist/telemetry.d.cts",
|
|
"default": "./dist/telemetry.cjs"
|
|
}
|
|
}
|
|
},
|
|
"scripts": {
|
|
"clean": "rimraf dist",
|
|
"build": "npm run clean && tsdown",
|
|
"build:dev": "npm run clean && tsdown",
|
|
"build:watch": "tsdown --watch",
|
|
"build:watch:prod": "tsdown --watch",
|
|
"test": "jest --coverage --watch --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.|__tests__/helpers/|\\.*manual\\.spec\\.\"",
|
|
"test:ci": "jest --coverage --ci --testPathIgnorePatterns=\"\\.*integration\\.|\\.*helper\\.|__tests__/helpers/|\\.*manual\\.spec\\.\"",
|
|
"test:cache-integration:core": "jest --testPathPatterns=\"src/cache/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
|
|
"test:cache-integration:cluster": "jest --testPathPatterns=\"src/cluster/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false --runInBand",
|
|
"test:cache-integration:mcp": "jest --testPathPatterns=\"src/mcp/.*\\.cache_integration\\.spec\\.ts$\" --coverage=false",
|
|
"test:cache-integration:stream": "jest --testPathPatterns=\"src/stream/.*\\.stream_integration\\.spec\\.ts$\" --coverage=false --runInBand --forceExit",
|
|
"test:cache-integration": "npm run test:cache-integration:core && npm run test:cache-integration:cluster && npm run test:cache-integration:mcp && npm run test:cache-integration:stream",
|
|
"test:s3-integration": "jest --testPathPatterns=\"src/storage/s3/.*\\.integration\\.spec\\.ts$\" --coverage=false --runInBand",
|
|
"verify": "npm run test:ci",
|
|
"b:clean": "bun run rimraf dist",
|
|
"b:build": "bun run b:clean && bun run tsdown",
|
|
"b:build:dev": "bun run b:clean && bun run tsdown",
|
|
"start:everything-sse": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/examples/everything/sse.ts",
|
|
"start:everything": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/everything.ts",
|
|
"start:filesystem": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/filesystem.ts",
|
|
"start:servers": "node -r dotenv/config --loader ./tsconfig-paths-bootstrap.mjs --experimental-specifier-resolution=node ./src/demo/servers.ts"
|
|
},
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "git+https://github.com/danny-avila/LibreChat.git"
|
|
},
|
|
"author": "",
|
|
"license": "ISC",
|
|
"bugs": {
|
|
"url": "https://github.com/danny-avila/LibreChat/issues"
|
|
},
|
|
"homepage": "https://librechat.ai",
|
|
"devDependencies": {
|
|
"@babel/preset-env": "^7.29.5",
|
|
"@babel/preset-react": "^7.18.6",
|
|
"@babel/preset-typescript": "^7.21.0",
|
|
"@langchain/langgraph": "^1.4.5",
|
|
"@rollup/plugin-alias": "^5.1.0",
|
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
"@rollup/plugin-json": "^6.1.0",
|
|
"@rollup/plugin-node-resolve": "^15.1.0",
|
|
"@rollup/plugin-replace": "^5.0.5",
|
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
"@types/bun": "^1.2.15",
|
|
"@types/express": "^5.0.0",
|
|
"@types/express-session": "^1.18.2",
|
|
"@types/jest": "^29.5.2",
|
|
"@types/jsonwebtoken": "^9.0.0",
|
|
"@types/multer": "^1.4.13",
|
|
"@types/node": "^24.12.4",
|
|
"@types/node-fetch": "^2.6.13",
|
|
"@types/react": "^18.2.18",
|
|
"@types/sanitize-html": "^2.13.0",
|
|
"@types/supertest": "^7.2.0",
|
|
"@types/winston": "^2.4.4",
|
|
"@types/yauzl": "^2.10.3",
|
|
"aws-sdk-client-mock": "^4.1.0",
|
|
"dedent": "^1.5.3",
|
|
"get-stream": "^6.0.1",
|
|
"jest": "^30.2.0",
|
|
"jest-junit": "^17.0.0",
|
|
"jszip": "^3.10.1",
|
|
"librechat-data-provider": "*",
|
|
"lodash": "^4.17.23",
|
|
"mammoth": "^1.11.0",
|
|
"mongodb": "^6.14.2",
|
|
"nanoid": "^3.3.7",
|
|
"pdfjs-dist": "^5.4.624",
|
|
"rimraf": "^6.1.3",
|
|
"rollup": "^4.34.9",
|
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
"sanitize-html": "^2.13.0",
|
|
"ts-node": "^10.9.2",
|
|
"tsdown": "^0.22.2",
|
|
"typescript": "^5.9.3",
|
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
"yauzl": "^3.2.1"
|
|
},
|
|
"publishConfig": {
|
|
"registry": "https://registry.npmjs.org/"
|
|
},
|
|
"peerDependencies": {
|
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
"@aws-sdk/client-bedrock-runtime": "^3.1013.0",
|
|
"@aws-sdk/client-cloudfront": "^3.1042.0",
|
|
"@aws-sdk/client-s3": "^3.980.0",
|
|
"@aws-sdk/cloudfront-signer": "^3.1036.0",
|
|
"@aws-sdk/credential-providers": "^3.1045.0",
|
|
"@aws-sdk/s3-request-presigner": "^3.758.0",
|
|
"@azure/identity": "^4.13.1",
|
|
"@azure/search-documents": "^12.0.0",
|
|
"@azure/storage-blob": "^12.30.0",
|
|
"@google/genai": "^2.8.0",
|
|
"@keyv/redis": "^4.3.3",
|
|
"@librechat/agents": "^3.2.57",
|
|
"@librechat/data-schemas": "*",
|
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
"@opentelemetry/api": "^1.9.0",
|
|
"@opentelemetry/instrumentation-express": "^0.56.0",
|
|
"@opentelemetry/instrumentation-http": "^0.207.0",
|
|
"@opentelemetry/instrumentation-ioredis": "^0.55.0",
|
|
"@opentelemetry/instrumentation-mongodb": "^0.60.0",
|
|
"@opentelemetry/instrumentation-mongoose": "^0.54.0",
|
|
"@opentelemetry/instrumentation-undici": "^0.18.0",
|
|
"@opentelemetry/resources": "^2.6.1",
|
|
"@opentelemetry/sdk-node": "^0.218.0",
|
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
"@smithy/node-http-handler": "^4.4.5",
|
|
"ai-tokenizer": "^1.0.6",
|
|
"axios": "^1.16.0",
|
|
"connect-redis": "^8.1.0",
|
|
"dedent": "^1.5.3",
|
|
"eventsource": "^3.0.2",
|
|
"express": "^5.1.0",
|
|
"express-session": "^1.18.2",
|
|
"firebase": "^11.0.2",
|
|
"form-data": "^4.0.4",
|
|
"get-stream": "^6.0.1",
|
|
"google-auth-library": "^9.15.1",
|
|
"https-proxy-agent": "^7.0.6",
|
|
"ioredis": "^5.3.2",
|
|
"js-yaml": "^4.2.0",
|
|
"jsonwebtoken": "^9.0.0",
|
|
"jszip": "^3.10.1",
|
|
"jwks-rsa": "^3.2.0",
|
|
"keyv": "^5.3.2",
|
|
"keyv-file": "^5.1.2",
|
|
"librechat-data-provider": "*",
|
|
"lodash": "^4.17.23",
|
|
"mammoth": "^1.11.0",
|
|
"mathjs": "^15.2.0",
|
|
"memorystore": "^1.6.7",
|
|
"mongodb": "^6.14.2",
|
|
"mongoose": "^8.23.1",
|
|
"nanoid": "^3.3.7",
|
|
"node-fetch": "2.7.0",
|
|
"pdfjs-dist": "^5.4.624",
|
|
"prom-client": "^15.1.3",
|
|
"rate-limit-redis": "^4.2.0",
|
|
"sanitize-html": "^2.13.0",
|
|
"sharp": "^0.33.5",
|
|
"undici": "^7.24.1",
|
|
"yauzl": "^3.2.1",
|
|
"zod": "^3.22.4"
|
|
},
|
|
"dependencies": {
|
|
"@langchain/langgraph-checkpoint": "^1.1.2",
|
|
"@langchain/langgraph-checkpoint-mongodb": "^1.4.0"
|
|
}
|
|
}
|