Carry conversations into new sessions with a visible summary
- Bots get continue_in_new_session: the bot writes a title and summary, the session closes for good (read-only) and the new one opens with the summary as a "Carried over from the last session" message that links back to the old session. - Continue in new session (conversation menu, sessions modal) and the automatic rollover at the session length do the same; New session now starts a fresh, unlinked session. - Session length is an app setting in Settings > General. - ?session_id= opens a session; a carried-over one shows a bar linking to where the conversation went on instead of the composer. - The thread can open the previous session above the current one (shown only, never in the prompt), and replies get search_past_sessions and read_past_session. - Sessions are named from their opening messages, not their latest, and a carried summary is labelled as the earlier session's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -8,6 +8,7 @@ import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabRunLimitsOverview from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
||||
import grabSkillLimitsOverview from "@/src/functions/backend/db/app-settings/grab-skill-limits";
|
||||
import grabHookCallRetentionOverview from "@/src/functions/backend/db/app-settings/grab-hook-events-keep-days";
|
||||
import grabSessionLengthOverview from "@/src/functions/backend/db/app-settings/grab-bot-session-max-messages";
|
||||
import grabStorageQuotaOverview from "@/src/functions/backend/db/app-settings/grab-storage-quota-overview";
|
||||
import { grabSystemPromptAppendOverview } from "@/src/functions/backend/db/app-settings/grab-system-prompt-append";
|
||||
import runningVersion from "@/src/utils/running-version";
|
||||
@@ -51,6 +52,7 @@ export default async function grabSettingsServerProps({ req }: Params) {
|
||||
run_limits: await grabRunLimitsOverview().catch(() => null),
|
||||
skill_limits: await grabSkillLimitsOverview().catch(() => null),
|
||||
hook_call_retention: await grabHookCallRetentionOverview().catch(() => null),
|
||||
session_length: await grabSessionLengthOverview().catch(() => null),
|
||||
running_version: runningVersion({ env: process.env }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from "react";
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import Form from "@moduletrace/twui/form/Form";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import type { SessionLengthOverview } from "@/src/types";
|
||||
import SettingsSaveBar from "../../(partials)/settings-save-bar";
|
||||
|
||||
type Props = {
|
||||
length?: SessionLengthOverview | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* How many messages a session takes, saved on its own without a reload.
|
||||
* Zero puts the code's default back.
|
||||
*/
|
||||
export default function SessionLengthForm({ length }: Props) {
|
||||
const [overview, setOverview] = useState(length || undefined);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [max_messages, setMaxMessages] = useState(String(length?.max_messages ?? ""));
|
||||
/**
|
||||
* The input only reads `defaultValue` when it mounts, so discarding or
|
||||
* saving remounts it with the saved number.
|
||||
*/
|
||||
const [form_key, setFormKey] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState<{
|
||||
msg?: string;
|
||||
error?: boolean;
|
||||
success?: boolean;
|
||||
}>();
|
||||
|
||||
const save = async () => {
|
||||
if (!window.confirm("Save how many messages a session takes? Sessions already past it move on at the user's next message.")) return;
|
||||
|
||||
setLoading(true);
|
||||
setStatus(undefined);
|
||||
|
||||
const res = await fetchApi<{}, APIResponseObject<SessionLengthOverview>>(
|
||||
"/api/admin/settings/session-length",
|
||||
{
|
||||
method: "POST",
|
||||
body: { max_messages: Number(max_messages) || 0 },
|
||||
},
|
||||
).catch(() => null);
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!res?.success || !res.singleRes) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res?.msg || `Couldn't save it.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOverview(res.singleRes);
|
||||
setMaxMessages(String(res.singleRes.max_messages));
|
||||
setFormKey((key) => key + 1);
|
||||
setDirty(false);
|
||||
setStatus({
|
||||
success: true,
|
||||
msg: `Saved: sessions move on after ${res.singleRes.max_messages} messages.`,
|
||||
});
|
||||
};
|
||||
|
||||
const discard = () => {
|
||||
setMaxMessages(String(overview?.max_messages ?? ""));
|
||||
setFormKey((key) => key + 1);
|
||||
setDirty(false);
|
||||
setStatus(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
save().catch(() => {});
|
||||
}}
|
||||
className="w-full gap-3! items-stretch"
|
||||
>
|
||||
<Input
|
||||
key={`messages-${form_key}`}
|
||||
label="Start a new session after"
|
||||
type="number"
|
||||
min={0}
|
||||
max={overview?.max}
|
||||
step={1}
|
||||
numberText
|
||||
rawNumber
|
||||
defaultValue={max_messages}
|
||||
onChange={(event) => {
|
||||
setMaxMessages(event.target.value);
|
||||
setDirty(true);
|
||||
setStatus(undefined);
|
||||
}}
|
||||
suffix={`messages`}
|
||||
info={`Counts the user's and the bot's messages. Fewer means cheaper replies but less said word for word; the summary carries the rest. 0 uses the default (${overview?.default_messages ?? ""}).`}
|
||||
/>
|
||||
<SettingsSaveBar
|
||||
dirty={dirty}
|
||||
loading={loading}
|
||||
status={status}
|
||||
save_label="Save"
|
||||
onDiscard={discard}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import isPassThroughUser from "@/src/utils/rbac/is-pass-through-user";
|
||||
import SettingsGroup from "../../(partials)/settings-group";
|
||||
import SessionLengthForm from "../(partials)/session-length-form";
|
||||
|
||||
/**
|
||||
* How many messages a session takes. Only the super admin sees it, since
|
||||
* it changes every conversation.
|
||||
*/
|
||||
export default function SessionLengthSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
if (!isPassThroughUser({ user_types: pageProps?.user_types })) return null;
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
title="Sessions"
|
||||
description="Every reply re-reads the session's recent messages, so a long session costs more tokens each time. Once a session has this many messages, the user's next message starts a new one, carrying a summary of the last. Bots can also move a conversation on sooner."
|
||||
id="sessions"
|
||||
>
|
||||
<SessionLengthForm length={pageProps?.session_length} />
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import InstallNameSection from "./(sections)/install-name-section";
|
||||
import NotificationsSection from "./(sections)/notifications-section";
|
||||
import PromptSection from "./(sections)/prompt-section";
|
||||
import RunLimitsSection from "./(sections)/run-limits-section";
|
||||
import SessionLengthSection from "./(sections)/session-length-section";
|
||||
import SkillLimitsSection from "./(sections)/skill-limits-section";
|
||||
import StorageSection from "./(sections)/storage-section";
|
||||
import VersionSection from "./(sections)/version-section";
|
||||
@@ -23,6 +24,7 @@ export default function AdminSettingsGeneralPage() {
|
||||
<NotificationsSection />
|
||||
<PromptSection />
|
||||
<RunLimitsSection />
|
||||
<SessionLengthSection />
|
||||
<SkillLimitsSection />
|
||||
<HookCallRetentionSection />
|
||||
<StorageSection />
|
||||
|
||||
Reference in New Issue
Block a user