Clear session links no carry-over made, at boot

Sessions from v0.2.8 and earlier were linked to the one before them by
every rollover and New session, so production threads offered "Show the
previous session" (and prompts read summaries) for sessions nothing was
carried from. A link is kept only when the session opens with a message
carried from that session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-26 06:21:51 +01:00
co-authored by Claude Opus 5.5
parent c7e5ed401b
commit 56f219b322
5 changed files with 67 additions and 1 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
### Bots, sessions, chat ### Bots, sessions, chat
- **Tables:** `bots` (`kind`, `color` `#rrggbb`, `stone`, `role`, `system_prompt`; `hue` is a legacy fallback), `bot_user_faces`, `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps` (unused yet). Enums in `src/dict/bot-dict.ts`. - **Tables:** `bots` (`kind`, `color` `#rrggbb`, `stone`, `role`, `system_prompt`; `hue` is a legacy fallback), `bot_user_faces`, `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps` (unused yet). Enums in `src/dict/bot-dict.ts`.
- **Sessions:** one thread per user per bot in its own chat plus one per workspace; split into `bot_sessions` (rollover at the app setting `bot_session_max_messages`, default `AppData.BotSessionMaxMessages`, via `grabBotSessionMaxMessages`; titled and summarised by `name-bot-session.ts` from the session's opening (`grabSessionOpening`; `grabBotMessages` reads the latest), summary carried into the next session only when they're linked by `previous_session_id`, which only a carry-over sets; older implicit links were cleared on 2026-10-26). New session (`startBotSession`) and a conversation with no active session always start fresh. The rollover carries over like the bot does (`rollOverBotSession` -> `continueBotSession`, falling back to the stored summary; kept for the next message while a job is open in the full session or there's nothing to carry), inside send-message, so the user's message lands under the summary. Helpers in `src/functions/backend/db/bot-sessions/`. Job results land in the job's own session, even after it closes, except a reply that called `continue_in_new_session` (`carryOverBotSession`; the menu's and sessions modal's Continue in new session does the same through `continueBotSession`, the model writing title and summary, refused while a job is open in the session): its summary is kept on the closed session and opens the new one as a bot message (`carried_from_session_id`), the job's `session_id` moves so the answer follows, the new session is titled by the bot then (it has no first exchange to be named from), and the old one can never be reopened (`reopenBotSession` refuses; a thread showing it swaps its composer for a link to the new one). Session lists (rail, sessions modal) reopen a session on click, as before; a carried-over one can't be, so it opens to read (`?session_id=`, `conversationHref`). The page tells them which session it shows (`announceShownSession` -> `useShownSession`): they reload, mark it Current (greyed out; without `?session_id=` the active one is), list it even when it's older than the rail's latest, and the active row then leads back. From half the session length the reply prompt suggests moving on (`longSessionNote`). Earlier sessions never enter the prompt (only the last one's summary): the thread can open them above a session (`useOlderMessages.loadPrevious`, a divider per session, quote and reply as usual), and replies get `search_past_sessions`/`read_past_session` (scoped to that user, bot and place, never the current session) when the conversation has any. - **Sessions:** one thread per user per bot in its own chat plus one per workspace; split into `bot_sessions` (rollover at the app setting `bot_session_max_messages`, default `AppData.BotSessionMaxMessages`, via `grabBotSessionMaxMessages`; titled and summarised by `name-bot-session.ts` from the session's opening (`grabSessionOpening`; `grabBotMessages` reads the latest), summary carried into the next session only when they're linked by `previous_session_id`, which only a carry-over sets; older implicit links are cleared at boot by `clearImplicitSessionLinks`). New session (`startBotSession`) and a conversation with no active session always start fresh. The rollover carries over like the bot does (`rollOverBotSession` -> `continueBotSession`, falling back to the stored summary; kept for the next message while a job is open in the full session or there's nothing to carry), inside send-message, so the user's message lands under the summary. Helpers in `src/functions/backend/db/bot-sessions/`. Job results land in the job's own session, even after it closes, except a reply that called `continue_in_new_session` (`carryOverBotSession`; the menu's and sessions modal's Continue in new session does the same through `continueBotSession`, the model writing title and summary, refused while a job is open in the session): its summary is kept on the closed session and opens the new one as a bot message (`carried_from_session_id`), the job's `session_id` moves so the answer follows, the new session is titled by the bot then (it has no first exchange to be named from), and the old one can never be reopened (`reopenBotSession` refuses; a thread showing it swaps its composer for a link to the new one). Session lists (rail, sessions modal) reopen a session on click, as before; a carried-over one can't be, so it opens to read (`?session_id=`, `conversationHref`). The page tells them which session it shows (`announceShownSession` -> `useShownSession`): they reload, mark it Current (greyed out; without `?session_id=` the active one is), list it even when it's older than the rail's latest, and the active row then leads back. From half the session length the reply prompt suggests moving on (`longSessionNote`). Earlier sessions never enter the prompt (only the last one's summary): the thread can open them above a session (`useOlderMessages.loadPrevious`, a divider per session, quote and reply as usual), and replies get `search_past_sessions`/`read_past_session` (scoped to that user, bot and place, never the current session) when the conversation has any.
- **Coordinator:** exactly one, the super admin's alone, created idempotently by `ensureCoordinatorBot`. Default prompt `src/data/coordinator-system-prompt.ts`, backfilled only when empty. - **Coordinator:** exactly one, the super admin's alone, created idempotently by `ensureCoordinatorBot`. Default prompt `src/data/coordinator-system-prompt.ts`, backfilled only when empty.
- **Routes:** `/admin/bots` reopens the last visited bot (cookie `cobble_last_visited_bot_id`, never a workspace conversation) if still reachable, else coordinator (super admin) / latest bot / first workspace / empty. `/admin/bots/[bot_id]` conversation (header, thread and composer at 760px, no right panel; `?session_id=` opens another of the user's sessions there: a closed one is reopened, except a carried-over one, which shows with `SessionMovedBar` instead of the composer, pointing where the conversation went on); `/admin/bots/[bot_id]/models`; `/admin/workspaces/[id]` dashboard; `/admin/workspaces/[id]/bots/[bot_id]` same conversation scoped to the workspace. - **Routes:** `/admin/bots` reopens the last visited bot (cookie `cobble_last_visited_bot_id`, never a workspace conversation) if still reachable, else coordinator (super admin) / latest bot / first workspace / empty. `/admin/bots/[bot_id]` conversation (header, thread and composer at 760px, no right panel; `?session_id=` opens another of the user's sessions there: a closed one is reopened, except a carried-over one, which shows with `SessionMovedBar` instead of the composer, pointing where the conversation went on); `/admin/bots/[bot_id]/models`; `/admin/workspaces/[id]` dashboard; `/admin/workspaces/[id]/bots/[bot_id]` same conversation scoped to the workspace.
- **Conversation loading:** pages render everything server-side (`grabConversationProps`) **except the history**, which the client loads (`api/admin/bots/conversation-history`, merged with what's already shown, refetched on bfcache restore) and pages older messages on scroll. Rail lists go through `railBots`/`railWorkspaces` (no prompts/memory, previews cut). While a reply is due the page polls `session-messages` (`use-reply-catch-up`) in case a push was missed. - **Conversation loading:** pages render everything server-side (`grabConversationProps`) **except the history**, which the client loads (`api/admin/bots/conversation-history`, merged with what's already shown, refetched on bfcache restore) and pages older messages on scroll. Rail lists go through `railBots`/`railWorkspaces` (no prompts/memory, previews cut). While a reply is due the page polls `session-messages` (`use-reply-catch-up`) in case a push was missed.
@@ -0,0 +1,32 @@
import { Database } from "bun:sqlite";
import { expect, test } from "bun:test";
import clearImplicitSessionLinksSql from "./clear-implicit-session-links-sql";
/**
* Only a carry-over keeps its link to the session before it. Run against
* real SQLite.
*/
test("clears links without a carried-over message", () => {
const db = new Database(":memory:");
db.run(`CREATE TABLE bot_sessions (id INTEGER PRIMARY KEY, previous_session_id INTEGER)`);
db.run(`CREATE TABLE bot_messages (id INTEGER PRIMARY KEY, session_id INTEGER, carried_from_session_id INTEGER)`);
db.run(`INSERT INTO bot_sessions VALUES (1, NULL), (2, 1), (3, 2), (4, 3)`);
db.run(`INSERT INTO bot_messages (id, session_id, carried_from_session_id) VALUES
(1, 2, 1),
(2, 3, NULL),
(3, 4, 1)`);
const { sql, values } = clearImplicitSessionLinksSql();
db.query(sql).run(...values);
const links = db.query(`SELECT id, previous_session_id FROM bot_sessions ORDER BY id`).all();
expect(links).toEqual([
{ id: 1, previous_session_id: null },
{ id: 2, previous_session_id: 1 },
{ id: 3, previous_session_id: null },
{ id: 4, previous_session_id: null },
]);
});
@@ -0,0 +1,20 @@
/**
* Clears `previous_session_id` on every session that wasn't carried over:
* before v0.3.0 a rollover and New session linked each session to the
* one before it, so threads offered "Show the previous session" and
* prompts read summaries of sessions nothing was carried from. A carry
* over always opens its session with a message naming the one it came
* from (`carried_from_session_id`); a link without one is cleared.
*/
export default function clearImplicitSessionLinksSql() {
return {
sql: `UPDATE bot_sessions SET previous_session_id = NULL
WHERE previous_session_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM bot_messages m
WHERE m.session_id = bot_sessions.id
AND m.carried_from_session_id = bot_sessions.previous_session_id
)`,
values: [] as number[],
};
}
@@ -0,0 +1,10 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import clearImplicitSessionLinksSql from "./clear-implicit-session-links-sql";
/**
* Drops session links from v0.2.8 and earlier that no carry-over made,
* safe to run on every start (`clearImplicitSessionLinksSql`).
*/
export default async function clearImplicitSessionLinks() {
await BunSQLite.sql(clearImplicitSessionLinksSql());
}
+4
View File
@@ -10,6 +10,7 @@ import fileIngest from "./file-ingest";
import liveRelay from "./live-relay"; import liveRelay from "./live-relay";
import enableForeignKeys from "./functions/backend/db/enable-foreign-keys"; import enableForeignKeys from "./functions/backend/db/enable-foreign-keys";
import migrateBotTypes from "./functions/backend/db/bot-types/migrate-bot-types"; import migrateBotTypes from "./functions/backend/db/bot-types/migrate-bot-types";
import clearImplicitSessionLinks from "./functions/backend/db/bot-sessions/clear-implicit-session-links";
import handleComputerProxy from "./functions/server/computers/handle-computer-proxy"; import handleComputerProxy from "./functions/server/computers/handle-computer-proxy";
import handlePwaFile from "./functions/server/pwa/handle-pwa-file"; import handlePwaFile from "./functions/server/pwa/handle-pwa-file";
import handleInstallIconFile from "./functions/server/pwa/handle-install-icon-file"; import handleInstallIconFile from "./functions/server/pwa/handle-install-icon-file";
@@ -34,6 +35,9 @@ await enableForeignKeys();
await migrateBotTypes().catch((error) => await migrateBotTypes().catch((error) =>
logError({ title: "Could not migrate bot types", error }).catch(() => {}), logError({ title: "Could not migrate bot types", error }).catch(() => {}),
); );
await clearImplicitSessionLinks().catch((error) =>
logError({ title: "Could not clear implicit session links", error }).catch(() => {}),
);
await bunext.bunextInit(); await bunext.bunextInit();
/** /**