mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
* feat: back AI sessions up to the workspace object storage Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SG5qEPM6Fmf7VerXS5nnWp * fix: bind the backup key to the user and pack pushes within the server caps Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SG5qEPM6Fmf7VerXS5nnWp * fix: keep refused and unavailable marks, one mark per key, stream the flush Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SG5qEPM6Fmf7VerXS5nnWp * fix: settle only fully sent sessions, keep removals while backups are off, cap pull bodies Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SG5qEPM6Fmf7VerXS5nnWp * fix: bound removal marks while backups are off and stale the sync rows instead of dropping them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: retry a lost lock, cap nested push lists and oversized pieces, drop a stale copy of a chat that outgrew the backup Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: cap pieces per push, size requests in UTF-8, keep a move's removal for an off workspace, disclose the restore counter Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: file a move's removal only once the new copy landed, retire marks through the sync row Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep a session marked while deletes are carried over, drop only gone sessions' marks Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: record what a refused flush already stored, stop early when every mark is retired Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: restore past another workspace's removal mark, file a move's removal before its row, bound the first pulled session Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: re-key the backups on workspace key rotation, accept only base64 images, carry a delete on the sync row when its mark cannot be written Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: durable conditional re-key of session backups, re-push on a storage switch Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: fail a push the key rotated under, settle no session split across storages, narrow the re-key module Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep a delete filed during a push, bound the pull and re-key listings Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: bound the session listing, mark the store's own user on a write that lands after a user switch Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: list sessions through per-session index markers, hold a session's parts back after a failed one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: record a rotation on every build, list a session only on the part that completes its push Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: leave an object larger than any push writes unread Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: read each object against its listed size, carry a dirty mark that cannot be written on the sync row Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: note the storages the re-key walk completed on, reach another user's rows on a failed mark, read a head at its cap Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: read the replaced key under its row lock, carry a refused dirty bump on the sync row Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: pull a session that outgrew one answer in pages, imported only whole Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: build a pull page from the smallest keys of the whole listing, stage each page as it lands Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: re-record a key rotated back to, admit earlier-page images, restage over a cut-short restore Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor: delete the backups on key rotation instead of re-keying them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: end a pull page before an object that grew since the listing, prune what a cut-short restore staged Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: delete the backups before the key commits, skip a planted object whatever its listing says, lock a restore across tabs, prune stale artifact versions Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep the backups under a prefix named by the key, delete the previous key's prefix after the commit Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: name the backup prefix by a generation the rotation bumps, never write an older record over a newer one on restore Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: retire a removal only against the storage holding the backup, restart a paged pull whose listing moved Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: fingerprint a pull page before reading it, answer the backup generation apart from the storage identity Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: answer needs_head for a headless session push, prune restaged pieces by id Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: serialize a session's push and removal, open whole pushes with the head, prune only own restores Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: require a head on a whole push, prune before the record lands, restore only under Web Locks Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: incremental pushes ride on a listed session, removals wait for every storage holding a copy Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: a whole push replaces the backup under a per-push token, a pull page is checked after its reads Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: fingerprint a pull page by entity tag and version too Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: a moved session's removal mark names the storages holding the old copy Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: restore a workspace family together, the newest copy of a moved session winning Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: name the marker by the session's move count, abort a family restore a listing failed in Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: list the family again before a restored record lands, require the pull fingerprint Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: list the whole family once per restored workspace, off members included Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: a push split over parts, incremental too, unlists the session until its last part Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: refuse a partial push part that names no push Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: keep a refused bump for a session with no row yet, probe an off workspace again Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: backfill row-carried bumps after a reload, ask an off workspace again on a timer Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: backfill a row for its bumps only when it carries some Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: a backfilled mark that cannot be written counts from the page's counter Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: say an off workspace is asked again, in the mirror's comments Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: update ee-repo-ref to 1c1dab33563c4907aff8b0da825fb66db60af82a This commit updates the EE repository reference after PR #796 was merged in windmill-ee-private. Previous ee-repo-ref: 289b477ca3fc993da06ec09b11c8f55d5e4e39c1 New ee-repo-ref: 1c1dab33563c4907aff8b0da825fb66db60af82a Automated by sync-ee-ref workflow. * fix: unlist a session while an incremental push changes more than one object Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
98 lines
4.0 KiB
TypeScript
98 lines
4.0 KiB
TypeScript
import { BROWSER } from 'esm-env'
|
|
import { userStore } from '$lib/stores'
|
|
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
|
|
|
|
// Browser-persisted AI-session state (the `/sessions` list, unread markers,
|
|
// chat-history IndexedDB, autonomy mode) must be scoped to the logged-in
|
|
// instance user so a shared browser never leaks one user's data to the next.
|
|
//
|
|
// The scoping identity is the instance user **email** — the only identity
|
|
// stable across a fork family (a single session deliberately spans multiple
|
|
// workspace_ids; `username` varies per workspace, `email` does not).
|
|
//
|
|
// The email arrives asynchronously (userStore is populated after the layout's
|
|
// getUserExt), while several consumers initialise eagerly at module import.
|
|
// This module owns the single subscription and lets each surface register a
|
|
// hydrate+migrate callback that fires once on registration (covering late
|
|
// registrants whose email already resolved) and again on every email change.
|
|
|
|
type UserChangeCallback = (email: string | undefined, prevEmail: string | undefined) => void
|
|
|
|
let currentEmail: string | undefined = undefined
|
|
const callbacks = new Set<UserChangeCallback>()
|
|
|
|
if (BROWSER) {
|
|
// userStore.subscribe fires synchronously with the current value (undefined
|
|
// at import time) and again whenever the user resolves / changes. Logout is
|
|
// a full page reload, and email is constant across workspace switches, so in
|
|
// practice the only transition is the initial undefined → email hydration.
|
|
userStore.subscribe((u) => {
|
|
const next = u?.email
|
|
if (next === currentEmail) return
|
|
const prev = currentEmail
|
|
currentEmail = next
|
|
for (const cb of callbacks) {
|
|
try {
|
|
cb(next, prev)
|
|
} catch (e) {
|
|
console.error('userScopedStorage: onUserChange callback failed', e)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Current logged-in user's email, or undefined before it resolves / when
|
|
// logged out.
|
|
export function getCurrentUserEmail(): string | undefined {
|
|
return currentEmail
|
|
}
|
|
|
|
// Namespace a base storage key (localStorage key or IndexedDB name) by the
|
|
// current user's email. Returns undefined when no user is known — callers must
|
|
// treat that as "do not read/write" so we never touch a browser-global key.
|
|
export function scopedKey(base: string): string | undefined {
|
|
if (!currentEmail) return undefined
|
|
return scopedKeyFor(base, currentEmail)
|
|
}
|
|
|
|
// The key a base name has for a given user, for work that captured its user up front and
|
|
// must not follow an in-place account switch (the session backup flush).
|
|
export function scopedKeyFor(base: string, email: string): string {
|
|
return `${base}::${email}`
|
|
}
|
|
|
|
// The email a scoped key or database name was built for, so a write that landed in a
|
|
// store can name the user it belongs to even after the current user changed.
|
|
export function emailOfScopedKey(base: string, key: string): string | undefined {
|
|
return key.startsWith(`${base}::`) ? key.slice(base.length + 2) : undefined
|
|
}
|
|
|
|
// Register a callback invoked whenever the scoping email changes. Fired once
|
|
// immediately with the current email (prevEmail undefined) so a surface that
|
|
// registers after the email already resolved still hydrates.
|
|
export function onUserChange(cb: UserChangeCallback): void {
|
|
callbacks.add(cb)
|
|
try {
|
|
cb(currentEmail, undefined)
|
|
} catch (e) {
|
|
console.error('userScopedStorage: initial onUserChange callback failed', e)
|
|
}
|
|
}
|
|
|
|
// One-shot claim of pre-namespacing data: if the scoped key is empty and a
|
|
// legacy un-namespaced key exists, move it under the scoped key and delete the
|
|
// legacy copy. The first user to log in on a previously single-user browser
|
|
// keeps their data; subsequent users start clean.
|
|
export function migrateLegacyLocalStorage(legacyKey: string, targetKey: string | undefined): void {
|
|
if (!BROWSER || !targetKey) return
|
|
try {
|
|
if (getLocalSetting(targetKey) != null) return
|
|
const legacy = getLocalSetting(legacyKey)
|
|
if (legacy == null) return
|
|
storeLocalSetting(targetKey, legacy)
|
|
storeLocalSetting(legacyKey, undefined)
|
|
} catch (e) {
|
|
console.error('userScopedStorage: legacy localStorage migration failed', e)
|
|
}
|
|
}
|