65cceaa67c
Persona chat stored a flat Vec<ChatMessage> keyed on (user_id, persona_id), which meant one rolling transcript per persona and no way to revisit a turn. This moves it onto the same ChatHistoryStore tree the file chat uses and gives conversations their own identity. Storage - messages_json now holds a serialized ChatHistoryStore. Reads accept the old flat array and upgrade it in place, so existing transcripts survive without a data migration. The upgrade drops the v1 seed greeting, which the flat renderer hid but the tree renderer would surface as a bubble the user has never seen. - New migration re-keys persona_chat_conversations on an opaque conversation_id and adds title + created_at, so one persona can hold any number of separate threads. Every DAO read and write is scoped by user_id as well: a conversation id is a bearer token for someone's transcript and must never grant access on its own. - The per-conversation lock and in-flight turn slot key on conversation_id, so two threads with the same persona can run turns concurrently. Endpoints - POST/DELETE /persona_chat/conversations — start and remove a thread, replacing /persona_chat/reset. - GET /persona_chat/conversations — the chat list, with snippet and counts derived from each tree's active branch. - POST /persona_chat/rewind, POST /persona_chat/switch-branch, GET /persona_chat/branches — rewind and fork, mirroring the file chat. Index 0 is rewindable here (it is the user's own first question, not a synthetic prompt) and re-anchors on the seed node. - history/turn/rewind/switch-branch/branches all key on conversation_id; history gained branch_id and now returns real fork_info, active_leaf_id and viewing_branch_id instead of placeholders. The turn body no longer carries a persona at all — it is read from the stored conversation, so a stale client cannot swap a thread's voice midway. Titles After the first turn persists, the conversation is named from its opening exchange on the same backend the turn ran on. Small models wrap titles in quotes, prefix them with "Title:" and append explanations, so sanitize_title strips all of that and truncates on a character boundary. Any failure falls back to the user's opening question. Generation runs after persistence: a failed title must not cost the turn. Fixes found along the way - insight_chat: both file-chat turn paths captured path.len() before apply_context_budget drained messages out of the middle, then sliced messages[path_len..] for the new tree nodes. Once truncation fired that dropped the user turn from the tree or panicked on an out-of-range start index. Now read after the budget pass as history_len. - Persona chat had no context budget at all and hardcoded truncated: false in the done frame, so a rolling transcript grew unbounded. - A cancelled turn persisted a half-finished transcript and pushed a second terminal frame; it now returns early like the file chat. - The seeded system prompt was frozen at conversation creation, so editing a persona never reached a thread already in flight. Re-resolved per turn. - turn_count was overwritten each write with the per-turn message delta; it is now the cumulative assistant-turn count on the active branch. - is_initial is always false: the file chat reserves it for its synthetic "describe this photo" prompt, and marking a persona chat's first question with it made the opening reply impossible to regenerate. 600 lib tests pass, clippy --all-targets clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
75 lines
2.2 KiB
TOML
75 lines
2.2 KiB
TOML
[package]
|
|
name = "image-api"
|
|
version = "1.5.0"
|
|
authors = ["Cameron Cordes <cameronc.dev@gmail.com>"]
|
|
edition = "2024"
|
|
|
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
|
|
[profile.release]
|
|
lto = "thin"
|
|
|
|
[profile.dev]
|
|
debug = "line-tables-only"
|
|
|
|
[dependencies]
|
|
actix = "0.13.1"
|
|
actix-web = "4"
|
|
actix-rt = "2.6"
|
|
tokio = { version = "1.42.0", features = ["default", "process", "sync", "macros", "rt-multi-thread"] }
|
|
actix-files = "0.6"
|
|
actix-cors = "0.7"
|
|
actix-multipart = "0.7.2"
|
|
actix-governor = "0.5"
|
|
futures = "0.3.5"
|
|
jsonwebtoken = "9.3.0"
|
|
serde = "1"
|
|
serde_json = "1"
|
|
diesel = { version = "2.2.10", features = ["sqlite"] }
|
|
libsqlite3-sys = "0.35"
|
|
diesel_migrations = "2.2.0"
|
|
chrono = "0.4"
|
|
clap = { version = "4.5", features = ["derive"] }
|
|
dotenv = "0.15"
|
|
bcrypt = "0.17.1"
|
|
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "rayon", "webp", "tiff", "avif"] }
|
|
infer = "0.16"
|
|
walkdir = "2.4.0"
|
|
rayon = "1.5"
|
|
path-absolutize = "3.1"
|
|
log = "0.4"
|
|
env_logger = "0.11.5"
|
|
actix-web-prom = "0.9.0"
|
|
prometheus = "0.13"
|
|
lazy_static = "1.5"
|
|
anyhow = "1.0"
|
|
rand = "0.8.5"
|
|
opentelemetry = { version = "0.31.0", features = ["default", "metrics", "tracing"] }
|
|
opentelemetry_sdk = { version = "0.31.0", features = ["default", "rt-tokio-current-thread", "metrics"] }
|
|
opentelemetry-otlp = { version = "0.31.0", features = ["default", "metrics", "tracing", "grpc-tonic"] }
|
|
opentelemetry-stdout = "0.31.0"
|
|
opentelemetry-appender-log = "0.31.0"
|
|
tempfile = "3.20.0"
|
|
regex = "1.11.1"
|
|
exif = { package = "kamadak-exif", version = "0.6.1" }
|
|
reqwest = { version = "0.12", features = ["json", "stream", "multipart"] }
|
|
async-stream = "0.3"
|
|
tokio-util = { version = "0.7", features = ["io"] }
|
|
bytes = "1"
|
|
urlencoding = "2.1"
|
|
zerocopy = "0.8"
|
|
ical = "0.11"
|
|
scraper = "0.20"
|
|
base64 = "0.22"
|
|
blake3 = "1.5"
|
|
image_hasher = "3.0"
|
|
bk-tree = "0.5"
|
|
async-trait = "0.1"
|
|
indicatif = "0.17"
|
|
uuid = { version = "1.10", features = ["v4", "serde"] }
|
|
|
|
# Windows lacks system sqlite3, so re-enable the bundled C build there.
|
|
# Linux/macOS use the system library (faster builds, smaller binary).
|
|
[target.'cfg(windows)'.dependencies]
|
|
libsqlite3-sys = { version = "0.35", features = ["bundled"] }
|