13 Commits
Author SHA1 Message Date
Timothy Jaeryang BaekandTaylor Wilsdon 088830cf82 feat: Dynamic Tool Loading / Lazy Loading
Co-Authored-By: Taylor Wilsdon <6508528+taylorwilsdon@users.noreply.github.com>
2026-10-10 22:43:52 +04:00
Timothy Jaeryang Baek ecbbff8afb refac 2026-10-09 10:22:37 +04:00
Classic298 fa66b0f306 fix: background sub-agent and timer replies only show in the open chat after a page reload (#31576)
When a sub-agent running in the background finished, or a timer the model set went off, the result and the model's follow-up reply were saved but did not show in the chat the user had open. They only appeared after a manual page reload. They now show in the open chat as soon as they arrive.

Fixes #31566
2026-09-30 19:03:01 +04:00
Classic298 b0650d04b2 fix: failed timer leaves a blank, unfinished reply in the chat (#31483)
When a scheduled timer failed before the model started answering, for example because its model had been removed, the chat showed the timer's prompt with a blank reply that looked stuck, and the error never appeared in the chat. The reply now shows the error and stops loading, like any other failed message.

Fixes #31481
2026-09-27 22:56:20 +04:00
Classic298 419d248093 refac: check the timer owner's role before running a due timer (#30220)
The due-timer executor rehydrates the owner from the database and now
verifies that the owner is still a user or an admin before entering the
chat completion pipeline, mirroring the check the scheduled-automation
executor already performs. A timer whose owner no longer qualifies is
recorded as an error instead of being run.
2026-09-21 10:46:48 -04:00
Classic298 16c2a9eda4 fix: index the chat queries that make large SQLite instances unusable (#27663)
The timer scheduler polls once a second and cancels on every message send and chat open, the sidebar lists chats ordered by `updated_at`, and the folder badges count unread chats per folder. None of those could be served by an index, so each call read most of the `chat` table, and because `meta` sits after the chat payload column SQLite had to walk every row's overflow pages to get there. On a large history that stalls the sidebar, every chat switch and every send, and the idle poll alone burns about a quarter of a CPU core.

Timers now keep their due time in a dedicated `chat.timer_at` column behind a partial index, and the chat list, unread and unfinished-reply queries each get an index matching their filter and ordering. Existing pending timers are backfilled from their meta by the migration. Dropping the `internal` and `type` checks also makes a forked timer chat inert, where a fork used to copy `meta` verbatim and become a second claim target that could fire a duplicate timer.

Measured on SQLite, same rows returned:

| query | before | after |
|---|---|---|
| idle timer poll (2000 chats, 0.43 GB) | 170 ms | 0.04 ms |
| cancel on send and chat open (4000 chats, 377 MB) | 200 ms | 0.04 ms |
| sidebar chat list (15000 chats, 1.26 GB) | 157 ms | 1.8 ms |
| folder unread badges (15000 chats, 1.4 GB) | 54 ms | 0.2 ms |

PostgreSQL 17 serves all of them as index-only scans with no sort node. Exercised through fresh install, upgrade with seeded data, downgrade and re-upgrade on SQLite and PostgreSQL 17.

Fixes #27622
2026-08-23 16:11:54 -05:00
G30 f5a5a434b9 fix: record an error state when a timer's chat completion raises (#27785) 2026-08-17 01:22:47 -06:00
Timothy Jaeryang Baek bb0f898b43 refac 2026-07-31 17:41:14 -04:00
Classic298 e140d8f3cc fix: scope timer cancellation to the timer's owner (#27472)
The events:chat socket handler called the ownership-checked update for last_read_at, discarded the boolean it returns, and then cancelled the chat's pending timers regardless of the answer. cancel_timers_for_chat selected on the internal marker, the type, the parent chat id and the status, and never on the owner, so it matched rows belonging to any user. An authenticated user who knew another user's chat id could mark that chat read over their own socket session and silently cancel the owner's pending timers, and the owner got no notification: the scheduled action simply never fired.

The missing owner predicate also cut the other way in ordinary use. Because the query matched every timer sharing a parent chat id, one user reading a chat cancelled the timers of anyone else holding one on the same chat, so this was collateral damage as much as an attack.

cancel_timers_for_chat now requires a user_id and filters on it, which is the durable fix, and the socket handler returns early unless the ownership-checked update reports that the caller owns the chat. The parameter is required rather than defaulted so a later caller cannot reintroduce the unscoped query by omission. Both existing call sites already know the acting user. Timer rows are created with the same owner as the parent chat and the execution path already refuses to run one whose owner does not match, so scoping the cancellation the same way cannot strand a timer that would otherwise have fired.

One behaviour change worth noting: an administrator posting into another user's chat no longer cancels that user's chat.user_message timers, because the acting user is the administrator. The timer fires instead of being cancelled, which is the safe direction.
2026-07-26 18:12:44 -04:00
Timothy Jaeryang Baek 49e57f4e7e chore: format 2026-07-20 22:11:42 -04:00
Timothy Jaeryang Baek 4d27bfff92 refac 2026-07-16 01:42:24 -04:00
Timothy Jaeryang Baek 771a024b40 refac 2026-07-16 00:39:31 -04:00
Timothy Jaeryang Baek b23ddeb280 refac 2026-07-16 00:30:44 -04:00