Replace per-component truncateName copies with the shared truncateRankName
from rankingChartLayout, and fix its ellipsis character to '…' (U+2026).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The language-preference endpoint calls createJiebaNlpProvider() which
uses the on-disk zh-CN dict. If the result was cached before the dict
was downloaded, the built-in fallback tokenization was served even after
the dict became available, until a DB write invalidated the cache.
Include the zh-CN dict file mtime/size as extraVersion in the cache
version so the entry is recomputed whenever the dict is added, replaced,
or deleted — consistent with the word-frequency fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no dict was downloaded, word-frequency results were computed with
the built-in tokenizer and cached. Downloading a dict cleared the in-
memory Jieba instance but left the cached result unchanged, so subsequent
requests returned the lower-quality built-in tokenization until the chat
DB was modified.
Add an extraVersion option to withAnalyticsCache and pass the dict file's
mtime/size as extraVersion for word-frequency requests so the cache is
invalidated whenever the dict file is added, replaced, or deleted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getDivingAnalysis and getNightOwlAnalysis derive values from the current
date (daysSinceLastMessage, currentStreak) that change even when the DB
file is untouched. Without a date component in the cache version, stale
results were returned until the next import or DB write.
Add a dailyInvalidate option to withAnalyticsCache that appends today's
date to the version string, and enable it for the diving and night-owl
endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The desktop worker kept a second analytics result cache in the same
cacheDir/query directory but invalidated it with a fragile scheme:
app-version .cache_version file, a dev-startup wipe, and explicit
invalidateAnalysisCache() calls after import. Replace it with the shared
getOrComputeAnalysisCache, fingerprinting entries by product version plus
the session DB file state, so the worker (still used by AI tools and the
desktop public REST API) and the shared HTTP routes share one cache and
one invalidation model. Fold the product version into the HTTP-route
fingerprint too, preserving the cross-release invalidation that
.cache_version used to provide, and drop the now-dead invalidation code.
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 time filter or active session while analytics requests are
in flight left obsolete requests pending, exhausting the browser's
per-origin connection budget and letting stale responses overwrite fresh
data. Introduce an AbortController-backed analytics request epoch
(analyticsGet/analyticsPost) that is reset whenever the filter or session
changes, silencing AbortError so cancelled requests never settle the UI.
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>
Analysis pages (private/group chat) are lazy-loaded routes that pull in
heavy deps (echarts, markdown-it, snapdom, vue-virtual, etc.) which Vite's
cold-start scan does not discover. The first navigation triggered an
in-flight dependency re-optimization, invalidating already-loaded chunks
and failing with 504 (Outdated Optimize Dep) on the dynamically imported
page module. This surfaced after a fresh pnpm install wiped the warm
node_modules/.vite cache.
Add optimizeDeps.include so the initial optimize pass already contains
these deps, preventing mid-session re-optimization and the 504.
Co-authored-by: Cursor <cursoragent@cursor.com>
The /_web/sessions route never called getAIChatCountsBySession() to
enrich session DTOs, leaving aiConversationCount always 0 in the
data management UI. Use the existing enrichSession callback and
AIChatManager to fill real counts for both list and detail endpoints.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep only the latest 3 versions in Cloudflare R2 to stay within
storage limits. Triggered automatically after each stable release
and manually via workflow_dispatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
In CLI Web mode, the frontend displays __APP_VERSION__ injected from the
root package.json at build time (0.26.2), while the server-side getVersion()
reads apps/cli/package.json at runtime (stuck at 0.25.1). This caused
buildWebUpdateCheckResult to compare remote 0.26.2 against local 0.25.1,
permanently reporting a false update notice even when the user was already
on the latest version.
Fix apps/cli/package.json to 0.26.2 to align with the product version.
Also extend set_release_version.sh to keep apps/cli/package.json in sync
with the root package.json on every release, preventing the drift from
recurring. apps/desktop/package.json is intentionally kept at 0.0.0 (the
desktop runtime uses app.getVersion() with __APP_VERSION__ fallback).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy favicon.ico from chatlab.fun and serve it via Vite's publicDir
(src/public/). Update index.html to reference the standard /favicon.ico
path; the previous /assets/imgs/favicon.ico path had no backing file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sync module creates its own PreferencesManager at server startup,
separate from the one shared by web routes. When the user configures their
identity through the web UI, the web routes' manager writes to disk and
updates its own cache, but the sync module's manager retains a stale cache
from startup. Subsequent pull imports then call tryApplyOwnerProfile with
the stale cache, returning no_profile and skipping owner assignment even
though a profile exists on disk.
Fix: invalidate the sync manager's cache before each onSessionImported
callback so the fresh profile is always read from disk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.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>
PullEngine gains an onSessionImported hook (hook errors never fail the
pull), wired in both the CLI server and Electron main to silently fill
session owner from the stored platform profile. The chatlab import CLI
command does the same after a successful import.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opening an unowned session first tries to auto-apply the platform
owner profile; only when that fails (and the session is not dismissed)
a lightweight searchable prompt pops up with Later / Don't remind /
Save actions. Member management now uses an entry card opening the
same modal, replacing the embedded OwnerSelector dropdown. Imports in
the session store silently retry profile application per session.
Co-Authored-By: Claude Fable 5 <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>
Multi-turn agent conversations rebuilt history as plain text, dropping
all tool calls and results. Later turns could not see what the model
had actually retrieved, so it imitated tool-free answers and
hallucinated chat records instead of re-querying (worse after
incremental imports and in long conversations).
- Persist provider-issued toolCallId, truncated result text and isError
on tool content blocks (written by the shared frontend store from
stream chunks; pi-agent-core events already carry these fields)
- Replay persisted tool blocks as real toolCall/toolResult message
pairs in toPiHistoryMessages: stable ids keep requests byte-identical
across turns (prompt cache friendly), unfinished blocks are skipped
so no toolCall is left unanswered, errors map to isError, results are
capped at 4000 chars, runtime-injected underscore params are stripped
- Legacy messages without persisted tool data fall back to the old
plain-text replay; no migration needed
Verified on a real CLI web session: 25+ replayed tool pairs accepted by
the provider across turns with zero API errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>