- Front-end error reporter was posting to /_web/_web/logs/report (double
base URL prefix); fixed to /logs/report
- uncaughtException/unhandledRejection handlers in CLI and Electron did not
exit after logging, overriding Node's default crash behavior; added
process.exit(1)
- NodePathProvider constructor in run() and chatlab status triggered
migrateFromElectronIfNeeded() as a side effect; added getSystemLogsDir()
to get the system logs path without resolving user data
- formatData() was called outside write()'s try/catch, so errors with
circular cause objects could escape appLogger.error(); moved inside
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Build a shared appLogger (node-runtime) that all three runtimes write to a
single rolling logs/app.log (10 MB → rename to app.old.log). Replaces the
four independent hand-written loggers (app.log/api.log Electron wrappers)
with a unified entry point while leaving AiLogger/perf-logger intact.
Key additions:
- packages/node-runtime/src/logging/app-logger.ts: initAppLogger/appLogger
singleton, INFO threshold by default (CHATLAB_LOG_LEVEL to override),
atomic rename rotation, Error data auto-extracts message+stack
- Three-runtime init: Electron main, CLI Web (http/index.ts), CLI (run())
- Global crash capture: uncaughtException + unhandledRejection on all three
Node runtimes now land in app.log scope 'crash'
- Fastify setErrorHandler fallback logs scope 'http' on CLI Web and Electron
internal server for unexpected 5xx errors
- Frontend: window.onerror + unhandledrejection + Vue errorHandler feed into
POST /_web/logs/report (scope 'web') with dedup+throttle
- AGENTS.md: logging rules section requiring key-path logs on new/changed features
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SemanticIndexSessionModal called act('disable') and referenced the
removed i18n key action.disable after the rename to remove; update
both the action call and i18n key.
- setConfig triggered preload even when saved.enabled is false; guard
preload behind saved.enabled so disabling the feature does not
download the local embedding model.
- worker-client setConfig returned early when transport was null, so
preload never ran in a fresh Electron session after model selection;
start the worker for local+enabled configs to ensure preload executes.
- rebuild() called computeDbPathHash(sessionId) directly instead of
resolveHash(), creating a duplicate state row for sessions stored
under a legacy full-dbPath hash; use resolveHash() and pass the
resolved hash to enableParams() to update the existing record.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace "停用" (disable, deferred cleanup) with "移除" (remove, immediate):
- Cancels any active build job
- Deletes vector chunks immediately (store.deleteByDbPathHash)
- Removes the state record entirely (stateStore.remove)
Applies to both the per-session button and the picker's deselect flow.
Fix legacy hash mismatch: old records were stored with a hash computed from
the full dbPath; current code hashes only the sessionId basename. Add
resolveHash() which falls back to a full-table scan by dbPath basename so
all operations (build/pause/cancel/remove/status/search) can find legacy
records regardless of which hash scheme was used.
Cap the enabled sessions list at max-h-72 with overflow-y-auto so the
settings page does not grow unboundedly with many sessions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move local embedding model storage from getCacheDir() to getAiDataDir()
(~/.chatlab/ai/models/semantic-index) so the "clear cache" button in
settings no longer wipes downloaded models.
Also trigger model download immediately in the background when a local
model is configured, instead of waiting for the first index build.
Download status (idle/downloading/ready/error) is tracked in
SemanticIndexService, exposed via the SemanticIndexRuntime interface and
worker RPC, and surfaced in the settings UI as a pulsing indicator.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "单次检索片段数" input was surfaced in settings but adds noise without
meaningful benefit for most users. Remove the UI control entirely and fix
normalizeConfig to always return SEARCH_MAX_RESULTS_DEFAULT (10), ignoring
any previously saved value so existing custom settings are silently reset to
default on next load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extract shared AnalyticsService to packages/node-runtime using direct
Aptabase HTTP API (removes @aptabase/electron dependency)
- Add POST /_web/telemetry/track route for generic event reporting
- Desktop: migrate analytics.json from userData to systemDir (~/.chatlab/),
trigger daily active from renderer via new IPC so user-selected locale is used
- CLI: inject APTABASE_APP_KEY at build time, create AnalyticsService on
server start, report app_active with platform: cli-web
- Both platforms report user-selected locale via platform service abstraction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a model service is renamed and its API key changed simultaneously,
onApiKeyCreated derives a new profile name from the new service name and
writes a new auth profile, but the old profile (keyed by the old name)
was never deleted, leaving an orphaned credential on disk.
Capture the old authProfile before applying updates. If onApiKeyCreated
returns a different profile name, call onApiKeyDeleted on the old config
to trigger cleanup before persisting the new profile.
No cleanup is needed for key-only updates (profile name unchanged) or
name-only updates (onApiKeyCreated is not called).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When deleteConfig was called, the auth profile written to
auth-profiles.json during addConfig was never cleaned up, leaving
orphaned credentials on disk indefinitely.
Add onApiKeyDeleted callback to LLMConfigStoreDeps and call it in
deleteConfig with the removed config so callers can clean up the
corresponding auth profile. Wire up deleteAuthProfile in both the
CLI HTTP server and the Desktop internal API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cast FakeSemanticIndexRuntime via unknown to satisfy the full
SemanticIndexRuntime interface in the factory return type.
Enable allowImportingTsExtensions in tsconfig.web.json so test files
importing .mts helpers pass vue-tsc type checking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
With API batched embeddings, insertChunks() and stateStore.updateProgress()
write to two separate SQLite databases, so they cannot be atomic. If the
worker is terminated between the two writes, the next resume attempts to
re-insert the same chunk_ids and hits the UNIQUE constraint, leaving the
session stuck in 'failed' state.
Use INSERT OR IGNORE on chunk_vector_index and skip the vec-table insert
when changes === 0. Duplicate inserts during resume are now silently
discarded instead of throwing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bring the Electron desktop client to parity with CLI web for semantic
index, and package the local embedding runtime correctly.
- Inject semanticIndexService into the desktop AI tool debug executor so
manual tool runs reach semantic search tools, matching the Agent stream.
Extract a platform-agnostic tool-executor-core with unit tests.
- Persist the API key by reference in the degraded semantic-index config
route (vector service unavailable) so API mode keys are not dropped;
make auth-profile read/write injectable and add route + unit regression
tests that never touch the real ~/.chatlab.
- Package desktop local embedding deps: declare @huggingface/transformers,
asarUnpack onnxruntime-node / @img native libs, and trim onnxruntime-node
binaries to the build target platform/arch to avoid shipping ~175M of
unrelated Linux/Windows binaries.
Co-authored-by: Cursor <cursoragent@cursor.com>
When sqlite-vec fails to load, use SemanticIndexConfigStore directly to
back the GET/PUT /_web/ai/semantic-index/config routes so users can still
configure the vector model from settings. Session management routes (enable,
build, search, etc.) remain unavailable since they require the vector store.
Export SemanticIndexConfigStore, SEMANTIC_INDEX_CONFIG_FILE, and
isSemanticIndexConfigured from node-runtime and semantic-index packages.
When sqlite-vec fails to load (e.g. native binding not found), the service
is null and routes were silently skipped, causing 404 errors in the
settings UI. Register read-only stubs for /config and /enabled so the
settings tab can open without errors when the service is unavailable.
Also export defaultSemanticIndexConfig from the node-runtime package root.
When buildAllPending finds a completed session whose live message count
has shrunk (e.g. after member deletion), enqueue rebuild instead of
skipping it, to remove stale vectors for deleted messages.
Expose hasNewMessages in listEnabledStatuses so the Settings UI pendingCount
correctly reflects sessions with appended messages, unblocking the
"Build Pending" button that was otherwise disabled for stale-completed sessions.
Allow API-mode semantic index configs without an API key when the base
URL does not require one (e.g. local Ollama); update settings UI to
reflect the keyless-allowed state and enable the session modal when a
keyless config is fully configured.
Prevent stale semantic index writes by checking write guard before
persisting chunks; make rebuild detection explicit on app restart via
reactivate() path; preserve full evidence snippet text without truncation;
add regression test for same-second chunk tie-breaking in mapMessageToChunk.
Session management:
- buildAllPending: recheck completed sessions against live message count
to enqueue rebuild when new messages arrived since last index run
- runner: detect non-append-only backfills and force full re-index instead
of resuming from a stale cursor that would skip the backfilled messages
- service/session-state-store: add reactivate() and use it when re-enabling
a stale session so the old model identity is preserved in the state row;
on restart buildAllPending() can still detect isStale() and enqueue rebuild
- chunker-config: use sessionId (not absolute path) as the stable key for
db_path_hash so migrating userDataDir does not orphan existing indexes
- cleanupUnused: cross-check state rows against listSessionIds() to detect
sessions whose DB was deleted and mark them pending cleanup
Retrieval accuracy:
- hasApiKey: guard against missing authProfile to avoid false positives
- FtsSearcher interface: return {id, ts} pairs; fts-searcher fetches ts from
message table so chunk mapping has the correct millisecond timestamp
- store.mapMessageToChunk: use (start_ts, start_message_id) composite ordering
and end-boundary check to break same-second ties deterministically
- message-range-reader: use (ts, id) composite bounds on both sides so
same-second messages at chunk boundaries are not attributed to wrong parents
- evidence.ts: add startTs/endTs to EvidenceHit; locate core evidence
boundaries by hit message IDs to avoid same-second ambiguity
- hybrid-search: expand ANN candidate pool to 10× when time filter is active
to reduce chance of missing in-range chunks in long conversations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For "what X did we discuss / which ones / what did we talk about" style
enumeration and topic-discovery questions, the model was brute-forcing
search_messages across many keyword-guess rounds instead of using vector
search. Strengthen the semantic-search guidance to send these questions to
semantic_search_current_chat and explicitly discourage multi-round keyword
guessing; restrict search_messages to known exact word/quote/sender/time.
Co-authored-by: Cursor <cursoragent@cursor.com>
Evidence messages carry millisecond timestamps but the preprocessing pipeline
renders times assuming seconds (multiplies by 1000), producing absurd years
(e.g. 56150) inside semantic snippets. Convert ms back to seconds before
rendering. Also strip the per-line time prefix from semantic snippets so the
evidence block shows a single group time instead of a time on every message,
and drop the redundant time span in the group header. Adds regression tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a structured "evidence" content block produced by retrieve_chat_evidence:
extract the payload from tool results, persist it in AI chat history, and
render grouped, desensitized sources in EvidenceBlock.vue. Sources open the
chat record drawer at the matched message for traceability. Evidence payload
types move to @openchatlab/core so tools, node-runtime and the frontend share
one definition. Includes zh-CN/en-US/ja-JP/zh-TW i18n.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add an "evidence" planner intent and tiered retrieval guidance so
fact/occurrence/count questions are routed to retrieve_chat_evidence
instead of a single low-level search path.
- planner: new evidence intent + prompt strategy suggesting only
retrieve_chat_evidence for that step
- semantic-search guidance: three-tier tool selection (evidence > semantic > keyword)
- expose retrieve_chat_evidence as an always-on Agent tool (desktop special
append; CLI via AGENT_TOOL_REGISTRY core), semantic tool stays dynamic
- export evidence tool + payload types from @openchatlab/tools
- TDD: planner evidence-intent test
Co-authored-by: Cursor <cursoragent@cursor.com>
Thread an optional timeFilter through SemanticSearchToolOptions →
searchForTool → search → hybridSearch so evidence retrieval no longer
mixes time ranges between semantic and keyword paths.
- hybridSearch filters dense + FTS-mapped chunks by startTs/endTs overlap
(millisecond, single-sided supported) and amplifies the candidate pool
when filtering to avoid under-recall
- retrieve_chat_evidence now passes its parsed ms range to the semantic path
- TDD: hybrid-search overlap unit tests + service wiring test
Co-authored-by: Cursor <cursoragent@cursor.com>
Add shared /_web/ai/semantic-index HTTP routes and wire the service into
CLI web and Electron internal API. Introduce the semantic_search_current_chat
agent tool (AGENT-only, not MCP) with desensitized output and a narrowed
service interface, forward the semantic-search context through the CLI and
Desktop tool adapters, and integrate on-demand semantic search into the
agent pipeline with guidance and a max-result-token evidence budget.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the deprecated always-on RAG system and introduce the new
semantic-index core: parent/child chunker with version/hash derivation,
embedding providers (local + OpenAI-compatible) and profiles, sqlite-vec
vector store, dense+FTS+RRF hybrid retrieval, evidence assembly, serial
warmup job queue and single-session runner, session business-state store,
and the SemanticIndexService that ties them together. Add an independent
config store and a dedicated vector directory via the path providers.
Co-authored-by: Cursor <cursoragent@cursor.com>
Repeatedly switching time filters recomputed expensive analytics/NLP
queries from scratch (e.g. a 1-year word cloud took 10s+ after a detour
through 5-year), because cancelling the network request cannot stop the
synchronous server-side computation. Add a shared, platform-agnostic
disk cache under cacheDir/query, validated by a fingerprint derived from
the session DB file (mtime+size of the main file and its -wal sidecar) so
any import, incremental import or member edit transparently invalidates
it. Wire it through the shared HTTP routes (word-frequency plus all
analytics endpoints), so CLI Web and the desktop internal server share
one implementation.
Co-authored-by: Cursor <cursoragent@cursor.com>
Switching the word count (80/100/150/200/300) in Insights > Topics >
Wordcloud re-ran a full-corpus jieba segmentation on the Electron main
thread, since topN only affects the final slice while the segmentation
cost is independent of it. This blocked the main process and froze the UI.
- Frontend: fetch the full word list once (max tier) and slice locally on
word-count change, recomputing percentages over the displayed subset.
Word-count switching no longer hits the backend or triggers segmentation.
- Backend: for Chinese meaningful/custom mode, produce word frequency and
POS-tag stats in a single jieba.tag pass instead of two, halving CPU for
genuine recomputes (filter/time/member changes). Behavior is preserved
(verified by an equivalence regression test).
Co-authored-by: Cursor <cursoragent@cursor.com>
On name-match platforms (WhatsApp/Line/Instagram), applyProfileToOtherSessions
writes the local member's platformId to each session, which can differ from the
source ownerId. The session store was caching the source ownerId for all updated
sessions, causing owner display misalignment before a full reload.
Fix: applyProfileToOtherSessions now returns {id, ownerId}[] so the service can
expose updatedSessionOwnerIds in the result; the store uses that map instead of
the single source ownerId.
Also fix a stale-cache issue: registerSessionRoutes and registerPreferencesRoutes
each created their own PreferencesManager when ctx.preferencesManager was absent
(CLI and Electron both omit it). Two independent caches caused a later PATCH to
preferences to deep-merge against stale data and overwrite ownerProfilesByPlatform
back to its pre-selection value. registerSharedRoutes now creates one shared
instance and passes it to all routes via resolvedCtx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared node-runtime service writes session owner, maintains the
per-platform profile in preferences.json, and batch-applies it to
other unowned same-platform sessions (unique match only, never
overriding an existing owner). Exposed via /_web/sessions/:id/owner/
{apply-profile,select,dismiss-prompt} for both Electron and CLI Web.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add deterministic owner matching in core (exact platformId first, then
normalized-name fallback limited to whatsapp/line/instagram), plus
OwnerProfile preferences fields shared across Electron and CLI Web.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AI was treating last_message_time as "when the group last had activity"
and drawing inferences like "the group has been quiet for two months,
maybe send a message to revive it." In reality, the timestamp only
reflects the end of imported data — the user may simply not have
imported newer records yet.
Add an inline parenthetical to the last_message_time field in all four
locale templates, and append a dedicated usage rule in
dataSnapshotUsageRules (zh-CN, en-US, zh-TW, ja-JP) explicitly
prohibiting inferences about group inactivity based on this value.
Also add the same note to formatDataSnapshotForPlanner's notes section
in data-snapshot.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Overview cache had no self-invalidation: once written it would return
stale lastMessageTs forever if the postImportHook failed silently.
Analysis query cache (17 chart/stats queries) was never cleared on
import, so charts showed stale data until the user triggered a manual
invalidate.
Add MAX(message.id) fingerprint to OverviewCache and introduce
getValidatedOverviewCache(), which recomputes automatically when new
messages are inserted. Update resolveOverview() in the Electron worker
to use this instead of bare getCache().
In both postImportHooks (incrementalImport, streamImport) also call
deleteSessionCache against the query cache directory so chart data is
fresh after every import.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Include assistant messages with tool content blocks in getHistoryForAgent
even when text content is empty, so multi-turn tool results are not lost
- Append Date.now() timestamp to export filenames to prevent silent overwrites
- Pass 600s timeout to exportFilterResultToFile worker call to match the
actual upper bound of large-session exports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend TokenUsage types across the full data pipeline to include
cacheReadTokens and cacheWriteTokens. The ChatStatusBar tooltip now
shows cache hit information when available. This is a generic
implementation that works with any API provider reporting cache metrics,
not limited to DeepSeek.
Co-authored-by: Cursor <cursoragent@cursor.com>
DeepSeek API requires reasoning_content to be retransmitted on assistant
messages that precede tool results. Add ReplayOptions to history replay
so thinking blocks are included as ThinkingContent when the model uses
DeepSeek-format thinking, preventing 400 errors in multi-turn tool-call
conversations.
Co-authored-by: Cursor <cursoragent@cursor.com>
Message-retrieval tools mirror rawMessages inside data, and the
preprocessing pipeline rendered that array into the tool result text as
"rawMessages: [object Object], [object Object], ..." — roughly 1.7KB of
junk per call (~400-500 wasted tokens) that also polluted the truncated
result text persisted for history replay.
- formatToolResultAsText skips the rawMessages key (raw message objects
are already rendered via the formatted messages list)
- CLI tool adapter strips rawMessages from extraDetails before invoking
the pipeline, matching the existing desktop adapter behavior
Regression tests fail without the fix at both the format layer and the
CLI adapter layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>