feat(chat): /new creates a new session instead of resetting the current one#3071
Conversation
|
Added a closely-related fix on the same branch (commit 0a8a311):
While testing
Verified end-to-end: |
|
Added a third related fix on the same branch (commit 209a513): Don't drop URL-pinned hand agents when Refreshing the dashboard on a hand-agent chat ( Fix: before clearing |
ReviewOverviewFeature design is good: splitting Blocking — scope creepThe diff silently bundles two unrelated changes not mentioned anywhere in the description: 1. Entire PR #3068 kernel-manifest fix is in this branch — 2. Backend3. Silent empty let sid = info
.get("session_id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();If 4. No test for the new cross-agent leak guard ( if session.agent_id != agent_id {
return (StatusCode::NOT_FOUND, ...);
}Check is correct — without it, 5. Untyped query struct (
Dashboard6. Silent override of Auto-enabling the hand-agents toggle whenever the URL points at an agent not in the current list is a real UX win (avoids kicking users to the default agent on refresh of a hand-agent URL), but it overrides a user-set localStorage toggle without any signal. Please mention it in the description — it's behaviour change the Summary skips. 7. i18n8. SDKsAll four get a breaking signature change on 9. Python None-handling def get_agent_session(self, id: str, session_id: Any = None):
return self._c._request("GET", f"/api/agents/{id}/session", None, query={"session_id": session_id})Depending on query = {"session_id": session_id} if session_id else NoneSame concern for Rust SDK's 10. Go SDK is a hard break — Test plan vs CLAUDE.mdOnly manual dashboard clicks are listed. Project instructions require Requested changes
Core feature design is good and the backend scoping check is the right move — once the scope is trimmed and the silent-failure cases are tightened, this is mergeable. |
…nt one BREAKING CHANGE: The `/new` slash command now creates a brand-new session (new session_id, empty history, registry switched to it) instead of resetting the current session in place. The previous behaviour was identical to `/reset`, leaving both commands functionally duplicated. - WS backend (ws.rs): `/new` now calls `kernel.create_agent_session` and returns the new `session_id` in the `command_result` payload. `/reset` continues to call `reset_session` (same id, history cleared). - Dashboard (ChatPage.tsx): `useChatMessages` surfaces a new `onNewSession(session_id)` callback. ChatPage wires it to update the URL (`?sessionId=<new>`), bump `sessionVersion`, and invalidate `agentKeys.sessions(...)` so the sessions dropdown refetches — URL and UI flip to the new session in a single round-trip. - Command registry (system.rs, en.json): `/new` and `/reset` now have distinct descriptions; `/reset` is listed separately in `/api/commands`. Migration: callers relying on `/new` to clear history without changing `session_id` should switch to `/reset`.
…nned tabs
When the dashboard sessions dropdown switched rows, the URL flipped to
`?sessionId=<X>` but the history fetch hit `/api/agents/{id}/session`
without forwarding the id — so the backend kept returning whichever
session the kernel had as canonical-active, which can drift away from
every row in the dropdown (e.g. after `/new` creates a fresh session
but the canonical-active pointer moves elsewhere). Result: no matter
which session you clicked, the chat pane showed whatever the kernel
thought was active, often a different session entirely.
- Backend (routes/agents.rs): `get_agent_session` now accepts an
optional `session_id` query param. If present it loads that session
via the memory substrate and validates `session.agent_id == agent_id`
before returning, so one agent's id cannot be used to read another
agent's history. Bad UUIDs → 400; absent param preserves the
canonical-active behaviour.
- Frontend (api.ts, ChatPage.tsx): `loadAgentSession(agentId, sessionId?)`
forwards the URL-pinned session id; `useChatMessages` passes its
`sessionId` argument through so the fetch matches the tab's URL
instead of the server's drifting active pointer.
Refreshing the dashboard on a hand-agent chat (`?agentId=<hand>`) would
dump the user back to the default assistant. The `agents` list is
derived from `useAgents({ includeHands: showHandAgents })`, where
`showHandAgents` is a localStorage toggle that defaults off. If it's
off, the server-side query excludes hand-spawned agents, the URL's
agent id isn't in the list, and the "clear missing selection" effect
flips `selectedAgentId` to "" — then the auto-select effect picks
"first running" = the default agent.
Fix: before clearing `selectedAgentId` because the agent isn't in the
list, flip `showHandAgents` on. The refetch pulls the hand agents in,
the URL-pinned id resolves, and the user stays on the chat they meant
to open. If the agent is still missing after hands are loaded, we
clear as before.
Side effect: landing on any hand-agent URL persists
`showHandAgents = "1"` in localStorage, so hand agents stay visible
in the picker going forward. That's the intended UX — the user
clearly wanted to interact with one.
…path
Replace `Query<HashMap<String,String>>` with a typed
`GetAgentSessionQuery { session_id: Option<Uuid> }` so a malformed UUID
is rejected by serde and returns a clean 400 — no hand-rolled parse
branch.
Also tightens the cross-agent leak guard. The original
`session.agent_id != agent_id` check only fired in the `Ok(Some)` arm,
but `get_session` returns `Ok(None)` when the row hasn't been
materialized yet (fresh-spawned agent, no messages). In that branch the
handler used to fall back to the agent's canonical-active session,
which let `GET /api/agents/A/session?session_id=<B's session>` succeed
with A's empty session under the requested id — visually a no-op, but
indistinguishable to a client from a successful read of an unrelated
session. Now: if an explicit session_id is provided in the query and
does not match this agent's canonical id, return 404. Same-agent
canonical id still round-trips (registry already proves ownership).
Adds `test_get_agent_session_rejects_cross_agent_session_id` covering:
- A's id + B's session_id → 404
- malformed UUID → 400
- A's id + A's own session_id → 200
The `/new` slash command swallowed an empty/missing `session_id` field via `unwrap_or_default()`, returning `command_result` with `session_id: ""`. The dashboard's truthy check then silently dropped the URL navigate, so the user saw "New session created." but the URL still pointed at the old session — exactly the silent-failure class this branch is otherwise fighting. Now: if `create_agent_session` returns success but the response payload has no usable `session_id`, surface a typed `error` event instead of a fake-success `command_result`.
Two small UX cleanups around the new session_id-pinned history fetch: - `useChatMessages` previously swallowed any `loadAgentSession` error with a comment-only catch. With the new server-side validation (404 cross-agent, 400 malformed uuid) a stale `?sessionId=` URL would render an empty chat with no signal. Forward the error message to the existing `onClearError` toast channel. - `zh.json` `cmd_new` still read "重置会话(清除历史)" — same string as `cmd_reset`. Updated to "新建会话(生成新会话 ID)" so the Chinese command palette reflects the new vs. reset split.
2c4ada5 to
68b75a2
Compare
|
Thanks for the thorough review @houko — addressed everything in a force-push ( Blocking
Backend
Dashboard
i18n
SDKs
Tests
PR body updated to call out the two undocumented behaviour changes. |
houko
left a comment
There was a problem hiding this comment.
All review concerns addressed and verified — #3068 manifest diff dropped, MAX_HISTORY_MESSAGES revert confirmed, /new emits explicit error on missing session_id, cross-agent guard test covers 404/400/200, typed GetAgentSessionQuery validates UUID, Ok(None) fallthrough fixed, Python/JS/Rust/Go SDK None-handling verified. LGTM.
Brings in PR librefang#3071 (rebased): /new creates a fresh session, GET /api/agents/{id}/session honors ?session_id=, hand-agent URLs auto- flip showHandAgents. Local-only ahead of upstream until librefang#3071 merges.
PR review on #3189 (houko) caught two stale references describing the pre-#3071 behavior of /new ('wipe the session' / 'clear history'). The new semantics fork off a fresh session while the previous one stays resumable on the same channel — phrasing updated in 4 places (en + zh × 2 files), plus a one-line clarifier in the slash-command roster section so the cross-reference doesn't silently inherit the old mental model: - configuration/channels: 'wipe the session' → 'fork off into a new session (the prior conversation stays resumable on the channel)' - integrations/api/realtime: '(clear history)' → 'forks into a fresh session — the prior session is preserved and remains resumable' - integrations/cli/commands roster paragraph: explicit note that /new is a fork, not a wipe. Same wording mirrored to docs/src/app/zh/.
…pi / observability (#3189) * docs(providers): add Novita / SearXHG / Bedrock entries * docs(config): document [parallel_tools] section + max_history_messages * docs(scheduler): cron multi-delivery + pre_script + silent_marker * docs: live context.md, chat attachments, CLI slash commands Salvages three of the six items from the (quota-killed) misc-features agent run. Each is a focused append, no rewrite of existing content. - agent/templates: live Live Context section explains the per-turn re-read of `context.md` (#3115), the .identity/ vs root path lookup, the 32 KB cap with stale-cache fallback, and the cache_context opt-out. - integrations/api/agents: attachment-resolution table (image / PDF / text+code) covering the supported MIME / extension list and the 200 KB truncation, plus a note about adjacent-text coalescing for small chat-tuned local models (#3094). - integrations/cli/commands: 'Slash Commands in Chat' section listing the registry-backed CLI commands (/model /status /help /clear /kill /exit) and pointing at channel-side commands for the broader set (#3122 #3123). * docs: tool invoke API, trajectory export, doctor audit, CLI default-pick, SDK codegen Picks up the items the second misc-features agent didn't get to before hitting the org quota. - integrations/api/system: POST /api/tools/{name}/invoke (#3025) — fail- closed allowlist, agent_id requirement for approval-gated tools. - integrations/api/agents: GET /sessions/{id}/trajectory (#3097) — json/jsonl formats, redactor pipeline, audit use case. - operations/troubleshooting: 'Audit checks' subsection enumerating the three registered AuditChecks (#3082) — VaultKey, ApiListenAddr, ConfigTomlSchema — with what each asserts and how to extend. - configuration/providers/tools: 'CLI logins as first-class default providers' section (#3061) — detection priority claude-code > codex- cli > gemini-cli > qwen-code, override via [default_model] in config.toml. - integrations/sdk: 'Auto-generated from openapi.json' section (#3046) — scripts/codegen-sdks.py, pre-commit hook, openai-tag skip rule. * docs(zh): mirror providers / config-core / cron / features-index sections * docs(zh): mirror agent / api / cli / providers-tools / troubleshooting / sdk additions Round 2 of the zh translation pass — picks up the seven mid-priority files that the first commit didn't cover. All sections mirror the EN content from #3189 but read naturally as Chinese (terms like agent / session / fallback / NoEntry left in English where they're API surface, prose translated to 中文). - agent/templates: Live Context section, .identity/ vs root fallback, cache_context = true opt-out. - api/agents: attachment-resolution table + trajectory export endpoint (#3025 / #3094 / #3097). - api/system: POST /api/tools/{name}/invoke endpoint + fail-closed allowlist explanation. - cli/commands: 'Slash Commands in Chat' section listing CLI registry commands and pointing at channel commands. - providers/tools: 'CLI logins as first-class default providers' detection priority and override. - troubleshooting: 'Audit checks' subsection + table. - sdk: 'Auto-generated from openapi.json' section. * docs: refresh /new slash-command semantics (#3071 review fix) PR review on #3189 (houko) caught two stale references describing the pre-#3071 behavior of /new ('wipe the session' / 'clear history'). The new semantics fork off a fresh session while the previous one stays resumable on the same channel — phrasing updated in 4 places (en + zh × 2 files), plus a one-line clarifier in the slash-command roster section so the cross-reference doesn't silently inherit the old mental model: - configuration/channels: 'wipe the session' → 'fork off into a new session (the prior conversation stays resumable on the channel)' - integrations/api/realtime: '(clear history)' → 'forks into a fresh session — the prior session is preserved and remains resumable' - integrations/cli/commands roster paragraph: explicit note that /new is a fork, not a wipe. Same wording mirrored to docs/src/app/zh/. * docs(channels): 9 channel-related Apr 22-25 PRs (en + zh) Covers the channel + bridge feature wave: - #3020 per-agent ChannelOverrides — fixes the contradicting 'without modifying the agent manifest' phrasing in both en and zh. - #3077 prefix_agent_name + Signal plain-text default - #3009 Slack reactions_enabled + #3005 Feishu @mention preservation consolidated into a 'Reactions and Processing State' section - #3008 Signal media attachments - #3012 WhatsApp send_voice + dm_policy / group_policy - #3011 Webhook deliver_only mode - #2972 channel file downloads (file_download_dir / max_bytes config) * docs(config-core): provider timeout / browser CDP / cron session caps (en + zh) Three new `config.toml` sections that landed Apr 22-23: - #3004 `[provider_request_timeout_secs]` map - #2993 / #2991 `[browser].cdp_endpoint` + `cdp_auth_token_env` - #2994 `[kernel].cron_session_max_tokens` / `cron_session_max_messages` * docs(api): GET /attach + Owner Notice envelope (en + zh) - #3078 SSE attach endpoint for multi-client co-watching — describes the per-session SessionStreamHub fan-out, attacher behaviour, and the curl example. - #2965 ReplyEnvelope.owner_notice + notify_owner tool — message body field, StreamEvent variant, WhatsApp OWNER_JIDS fan-out. * docs(observability + ops): Tempo stack, cache_hit_ratio metric, daemon log tee (en + zh) - #3064 + #3149 — bundled observability stack (otel-collector / Tempo / Grafana), bring-up commands, [telemetry] config, business spans, cache_hit_ratio metric formula and where it surfaces - #3022 — `librefang start --foreground` now tees logs to ~/.librefang/logs/daemon-YYYY-MM-DD.log; same-day append; 7-day rotation pruned on daemon start * docs: hooks transform / lazy tools / mcp taint / capability detection / providers extras Final batch of catch-up docs covering the remaining Apr 22-25 user-facing features: - agent/hooks: transform_tool_result hook contract (#3003) - agent/templates: tool_search / tool_load lazy loading + lazy_tools opt-out (#3047) - integrations/mcp-a2a: TaintRuleId enum + per-tool skip_rules policy (#2999) - configuration/features: compaction summaries in user's conversation language (#3007) - configuration/providers/local: embedding auto-detection priority list (#3099) + Ollama capability detection (Modality / metadata pipeline, #3074 / #3133 / #3134 / #3140) - configuration/providers/management: custom image-gen provider via generic OpenAI-compat driver (#2998) + Moonshot file-upload helper via /v1/files (#2966) All sections mirrored to docs/src/app/zh/. * docs: address houko fix-PR audit (8 items, en + zh) PR review caught 1 stale-doc contradiction + 7 undocumented behaviour changes from the fix-PR cohort. - #3114 — configuration/core: 'api_key empty = unauthenticated' was stale; now documents the loopback-only policy (non-loopback bypass was closed) - #3170 — features/observability: auto_start is opt-in; home_dir- scoped Docker labels; RAII cleanup - #2989 — api/agents POST /message: per-request session_id override - #3048 — Interrupting a Running Agent Turn: /stop cascades into agent_send subagents (recursive) - #3056 + #3000 — desktop: dedicated Connection screen via custom URI scheme with retry / open-in-browser / uninstall - #2952 — triggers: response routes to agent's home channel - #2955 — triggers: task_posted assignee_match = 'self' | 'any' - #3069 — providers/local: 60-second reprobe + manual 'Test connection' refresh All sections mirrored in docs/src/app/zh/.
Summary
Splits
/new(fresh session, new UUID, navigate) from/reset(wipe in place, same UUID). Also adds two related fixes for URL-pinned sessions discovered while testing.Behaviour changes
/newcreates a new session instead of clearing in place. Channels/scripts that relied on the old behaviour should switch to/reset.GET /api/agents/{id}/sessionnow accepts?session_id=<uuid>to load a specific session. Cross-agent reads return 404; malformed UUIDs 400.showHandAgentsto"1"in localStorage so the picker keeps showing hand agents on refresh.Changes
ws.rs— splitnew/reset;/newnow surfaces an explicit error if the kernel returns nosession_idinstead of swallowing it.routes/agents.rs— typedGetAgentSessionQuery { session_id: Option<Uuid> }; cross-agent guard now also fires in theOk(None)branch (an explicitsession_idthat doesn't match this agent's canonical id 404s rather than falling back to the canonical empty session).ChatPage.tsx—useChatMessageshonors the URL-pinnedsessionId; auto-flipsshowHandAgentsfor hand-agent URLs; surfaces session-load errors via the existingonClearErrortoast channel.zh.json—cmd_newnow reads "新建会话(生成新会话 ID)".go,js,python,rust):get_agent_sessiongains an optionalsession_idquery param. Helper functions in all four SDKs already dropNone/undefined/empty values, so default callers are unaffected.Tests
cargo test -p librefang-api: 42 passed (includes newtest_get_agent_session_rejects_cross_agent_session_idcovering the cross-agent 404 + malformed-UUID 400 + same-agent round-trip).cargo clippy --workspace --all-targets -- -D warnings: clean.cargo test --workspace: 1 pre-existing failure (librefang-kernel::config::tests::test_load_config_defaults) — host-env dependent (asserts defaultlog_level == "info"but reads from~/.librefang/config.toml), identical onupstream/mainunmodified.Test plan
/newflips URL?sessionIdto a fresh UUID, dropdown shows the new row, history empty./resetstill clears history in the current session (samesession_id).GET /api/agents/{A}/session?session_id=<B's session>→ 404.?session_id=foo→ 400.?agentId=<hand-agent>no longer dumps to default assistant.