* 🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile
The main hash accepted any npm module that happened to resolve inside apps/desktop/node_modules, so a module linked from outside the frozen install (or from a stale store entry) could enter the renderer OTA hash without the lockfile ever pinning it.
Resolve each bundled module to its package name@version and require apps/desktop/pnpm-lock.yaml to pin it, keeping the file:/link:/git/tarball escape hatch for specifiers that pin by resolution instead of version.
partial-json is imported by packages/utils and packages/context-engine but was never declared, so the Desktop lockfile could not pin it: declare it in both packages and record it in apps/desktop/pnpm-lock.yaml.
* 🔊 chore(desktop): name the changed bundles before a dev restart
The dev orchestrator logged only that a main/preload bundle changed, which cannot distinguish the debounced restart from an unrelated vite rebuild. Collect every file reported by the watcher during the debounce window and print it with its event type, size and mtime.
* 📝 docs(acceptance): publish the round from the session's own observations
A delivery verified on the real product earlier in the session still had to re-run the plan and checker stage to obtain a published round. Document that those observations and artifacts are the evidence: write plan[] and cases[] from them, disclose anything never captured, and ingest.
* ⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path
Four package-level import edges put ~1.5 MB raw on the first screen:
- GlobalProvider/Editor.tsx imported @lobehub/editor/react, and rolldown does
not shake that entry down to EditorProvider, so lexical, yjs and fuse landed
in the entry chunk. The provider is reached by path until the package
exposes a provider-only entry.
- @lobechat/types re-exported the ajv-backed AgentGraphSchema through the
barrel that every store touches; it now lives on the
@lobechat/types/agent/graph subpath and the agent barrel re-exports types
only.
- @lobechat/context-engine's root barrel dragged the database models barrel
(drizzle) in through AgentDocumentInjector; the app reaches the
./agent-document subpath and the injector reads loadPolicy directly.
- packages/locales bundled locales/en-US/*.json on top of the default
sources it mirrors; the overlay now applies only in development.
Web production build: first-screen gzip 2299 KB -> 1968 KB.
Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH
* 🐛 fix(vite): keep one EditorContext in dev for the path-imported provider
Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH
* 🐛 fix(locales): refresh consumers after the dev English overlay lands
Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH
* 🐛 fix(tools): make TOOL_NAME_MAX_LENGTH=0 actually disable tool-name compression
`TOOL_NAME_MAX_LENGTH` (#17249) is read from `process.env` at module load in
`ToolNameResolver`, so it only ever applied to the gateway (server-run) path.
The default client-driven chat path builds the tool payload in the browser,
where the server env is invisible — deployments setting `0` still got opaque
`MD5HASH_…` tool names.
Ship the value to the client with the global server config and push it into the
context-engine module both on config load and at `createToolsEngine` (the client
mirror of `createServerToolsEngine`), before any tool name is generated.
Also adds the var to the `@lobechat/env` tools schema and documents it in the
self-hosting env docs (en + zh-CN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ♻️ refactor(tools): parse TOOL_NAME_MAX_LENGTH through one shared function
Review follow-up: the env schema coerced the value with zod while
`ToolNameResolver` parsed `process.env` with `parseInt`, so the same env value
could mean two things on the two sides that generate tool names:
raw resolver (server) shipped to client
1e2 1 100
" " 64 (fallback) 0 → compression silently off on the client
2048 2048 throws
-5/abc 64 (fallback) throws
The last row is the worst: `@t3-oss/env-core` throws on validation failure, so a
typo'd value would take `getServerGlobalConfig()` down entirely, where before it
just fell back to 64.
Move the parse into `parseToolNameMaxLength` (`@lobechat/const/plugin`, a leaf
module both sides can import cheaply — importing context-engine here would drag
the database module graph into the config path) and have the resolver and the
global config both use it. Unset / unparseable / negative reads as "not
configured" and falls back to the default; no upper bound, matching the
resolver. Env schema now passes the raw string through.
Covered by a table test in `ToolNameResolver.config.test.ts` (parse semantics)
and one in `getServerGlobalConfig.test.ts` (the shipped value matches, and a
typo no longer throws).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ♻️ refactor(tools): keep the tool-name config out of the app-shell bundle
The client-side sync lived in `serverConfig`'s SWR success handler, which put a
static `@lobechat/context-engine` import into a store that every page creates —
including the lightweight auth / popup shells — dragging the engine's module
graph (down to `@lobechat/database`) into bundles that never generate a tool
name.
Keep the value as plain data in the server config and push it into the engine
from the sites that actually generate names, via `applyToolNameMaxLength()`:
`createToolsEngine` (the tools payload) and the agent-profile mention list,
whose generated names are written into the system prompt and so must match the
names sent to the model. `src/store/serverConfig` is now byte-identical to
canary.
Verified: SPA vite build passes (the `@lobechat/const/plugin` subpath resolves
in the client bundle); context-engine 445 tests, client-side 286 tests, server
globalConfig/tool-engine suites, and the full type-check all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🐛 fix(context-engine): account for tool_calls + reasoning + tool defs in compression budget
The pre-compression token check (`shouldCompress`) only counted `msg.content`,
which under-counted typical agent conversations by ~58% — tool_calls (~33%
of payload), reasoning traces (~17%), and top-level tool definitions (~2%)
were all silently ignored. As a result, conversations that the provider
tokenizer measured at ~656K passed the harness's 524K threshold without
firing compression, and were rejected upstream as ExceededContextWindow.
Verified empirically against 2 op snapshots in the same topic that hit
the failure mode (LOBE-8964): harness counted 267K, deepseek measured
649K — a 380K (58.8%) gap. ~92% of that gap is fixable by accounting
for the missing fields; the remaining ~8% is `tokenx` vs provider
tokenizer drift, compensated by a 1.25× multiplier on the trigger path.
Changes:
- New `@lobechat/context-engine/tokenAccounting` module exporting
`countContextTokens({messages, tools, options})`. Returns structured
per-source + per-message + per-tool breakdown — usable both by the
compression trigger and by UI panels showing "context by type".
- `shouldCompress` in agent-runtime delegates to `countContextTokens`,
applies the 1.25× drift multiplier on `adjustedTotal` for the trigger
decision, exposes raw count via `currentTokenCount`. Signature now
takes `UIChatMessage[]` directly.
- Removed deprecated `calculateMessageTokens` / `estimateTokens` /
`TokenCountMessage` from agent-runtime — the new module supersedes
them. `createAgentExecutors.ts` updated to call `countContextTokens`
directly for post-compression telemetry.
- Added `raw-md` plugin to agent-runtime vitest config (needed once
context-engine is imported transitively, since the import graph pulls
in `@lobechat/agent-templates` `.md` files).
What's intentionally NOT counted (DB-only fields not sent to provider):
`plugin`, `pluginState`, `chunksList`, `extra`, `fileList`, etc.
Counting these would over-estimate and trigger compression too early.
Tests:
- 19 new unit tests for `countContextTokens` covering content / tool_calls
/ reasoning / tool_call_id / tool definitions / fast-path / aggregation
/ DB-only field exclusion.
- `tokenCounter.test.ts` updated for new drift semantics + UIChatMessage
signature; one boundary case now triggers compression (intentional —
the drift multiplier kicks in at the threshold).
Refs: LOBE-8964 (ECW edge boundary), LOBE-8972 (ECW umbrella),
LOBE-8973 (openrouter `:free` ctx), LOBE-8976 (compression diagnostics).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>