This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/CHANGELOG.md
Jeffrey Smith 071dea8904 Changeset 0.31.0 (#203)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
2026-03-19 00:06:16 +00:00

236 KiB
Raw Blame History

Changelog

[0.31.0] — 2026-03-18

Summary

Editor package: the code editor surface is now an installable .pkg file (type full, tier browser). Zero platform special-casing — validates the entire v0.28.7v0.30.2 extension stack. The editor package lives in packages/editor/ and is installed via admin UI upload. Core editor surface, template, CSS, JS, and data loader removed.

New

  • Editor .pkgpackages/editor/ contains manifest.json, js/main.js, and css/main.css. Type full, tier browser. JS-only rendering replaces server-rendered Go template partials. Dynamic script loading for CodeMirror bundle and ChatPane. Route: /s/editor?ws=<id>.
  • Editor settings — manifest declares font_size, tab_size, word_wrap settings configurable via admin packages UI.
  • UI state persistence — open tabs, active tab stored in localStorage per user per workspace (debounced 2s save).
  • E2E testscrud/editor-package.js — install, type/tier verification, settings CRUD, export/import round-trip, core removal verification.

Removed

  • surface-editor Go template (server/pages/templates/surfaces/editor.html)
  • editor-surface.js and editor-surface.css (replaced by package assets)
  • EditorPageData struct and editorLoader function
  • Hardcoded /editor sidebar link (now rendered by extension nav loop)
  • /editor nginx location rule (served via /s/editor)
  • Editor entry from registerCoreSurfaces() and extensionNavItems() skip

Changed

  • SeedSurfaces() now cleans up the old editor core surface row to prevent broken nav links before the package is installed.

[0.30.2] — 2026-03-18

Summary

Workflow packages: workflows can be exported as .pkg bundles and imported on other instances. Custom stage surfaces load from installed packages via surface_pkg_id. Starlark workflow.* module provides sandbox access to workflow state. ICD test suite hardened (auth rate limiter, vault UEK pre-warm). 600 tests, 0 failures.

New

  • Workflow package export/importGET /admin/workflows/:id/export bundles a workflow definition + stages as a downloadable .pkg ZIP. POST /admin/packages/install with type: "workflow" recreates workflow definition + stages from the manifest. Round-trip tested in ICD suite.
  • Custom stage surfacessurface_pkg_id column on workflow_stages links a stage to an installed package's JS surface. Template loads the package's main.js via dynamic script import and mounts via WorkflowSurfaces.mount(). Falls back to built-in mode rendering when no custom surface is assigned.
  • Starlark workflow module — sandbox builtins: workflow.get_definition, workflow.get_stage_data, workflow.advance, workflow.reject. Gated by workflow.access permission.
  • sw.workflow SDK namespacemount(), registerSurface(), getContext(), advance(), reject() for frontend surface authors.
  • Admin workflow builder — visual stage editor with DnD reorder, form field builder (8 types), export/import buttons, surface package selector. No JSON editing required.
  • ICD test fixes — auth rate limiter burst 30→8 (transport test expects 429 on 10th rapid login). Vault UEK pre-warm on startup via variadic ProbeAndRepairVault(*crypto.UEKCache).

Changed

  • workflow_stages.surface_pkg_id column added (migration 018, PG + SQLite)
  • packages.source CHECK widened to include 'registry' (migration 016)
  • Auth rate limiter: (5, 30)(5, 8) — do not increase burst above 9
  • ICD test count: 579 → 600 (with Venice BYOK configured)

[0.29.2] — 2026-03-17

Summary

DB extensions and server-side tool execution. Starlark packages declare namespaced database tables via db_tables in the manifest — created on install, dropped on uninstall. A structured Starlark db module (query/insert/update/delete/view/list_tables) provides namespaced access to extension-owned tables and column-allowlisted platform views. Server-side tool execution wires the on_tool_call entry point into the completion tool loop: extensions declare tools in their manifest, the tool loop dispatches matched calls to the sandbox. Three changesets (CS0CS3) plus docs (CS4).

New

  • db Starlark module — structured API for extension-owned tables. Requires db.read permission (query/view/list_tables) or db.write (insert/update/delete). Tables namespaced as ext_{pkg_slug}_{name}.
    • db.query(table, filters=None, order=None, limit=100) → list of dicts
    • db.insert(table, row_dict) → inserted row dict (auto-generates id)
    • db.update(table, id, partial_dict) → True
    • db.delete(table, id) → True
    • db.list_tables() → list of logical table names for this package
    • db.view(view_name, filters=None, limit=100) → list of dicts (allowed views: "users", "channels")
  • db_tables manifest key — declare extension-owned tables with typed columns and indexes. Tables created on install, dropped on uninstall.
    {
      "db_tables": {
        "logs": {
          "columns": {"message": "text", "user_id": "text", "count": "int"},
          "indexes": [["user_id"]]
        }
      }
    }
    
    Supported column types: text, int/integer, real/float, bool/boolean, timestamp. Auto-columns: id TEXT PRIMARY KEY, created_at (dialect-correct default). Dialect-correct DDL for both PG (TIMESTAMPTZ, BOOLEAN) and SQLite (TEXT, INTEGER).
  • Platform views — column-allowlisted read-only views over platform tables accessible via db.view(...):
    • ext_view_users: id, display_name, email
    • ext_view_channels: id, name, type, team_id
  • ext_data_tables catalog — tracks logical→physical table mappings per package. Powers db.list_tables() and uninstall cleanup.
  • Server-side tool execution — starlark extensions declare tools in the manifest. BuildToolDefs includes starlark-tier active package tools alongside server tools. CoreToolLoop dispatches matched calls to executeExtensionTool which calls the on_tool_call entry point.
    {
      "tools": [{
        "name": "search_logs",
        "description": "Search extension log entries",
        "parameters": {
          "type": "object",
          "properties": {"query": {"type": "string"}},
          "required": ["query"]
        }
      }]
    }
    
    Entry point:
    def on_tool_call(call):
        # call = {"tool_name": "...", "tool_call_id": "...", "arguments": {...}}
        if call["tool_name"] == "search_logs":
            rows = db.query("logs", filters={"user_id": call["arguments"]["user_id"]})
            return {"results": rows}
        return {"error": "unknown tool"}
    
  • BuildExtToolMap — pre-loads map[toolName]*PackageRegistration per request to avoid DB lookup on each tool dispatch.
  • ExtDataStore interface + implementations (postgres, sqlite) — RegisterTable, ListTables, DeletePackageTables.
  • db.read and db.write permissions — active in this release (previously listed as "future").

Changed

  • LoopConfig gains Runner *sandbox.Runner and ExtTools map[string]*store.PackageRegistration for extension tool dispatch. All callers updated: streamCompletion, syncCompletion, multi-model path, messages.go regen, scheduler executor.
  • BuildToolDefs (resolve.go) now includes starlark-tier active package tools in addition to browser-tier tools.
  • streamWithToolLoop and streamModelResponse accept runner and extTools parameters.
  • CompletionHandler gains SetRunner(r *sandbox.Runner) and buildExtToolMap helpers.
  • AdminInstallExtension and AdminUninstallExtension call CreateExtTables/DropExtTables for schema lifecycle.
  • InstallPackage and DeletePackage (packages.go) call the same schema lifecycle hooks.
  • Migration 016_packages.sql extended with ext_data_tables catalog table, idx_ext_data_tables_pkg index, and ext_view_users/ ext_view_channels platform views.
  • Runner.SetDB(db, isPostgres) wires the DB handle for buildModules. main.go calls starlarkRunner.SetDB at startup.
  • Extension permissions table in docs updated: db.read and db.write now listed as v0.29.2 (previously "future").
  • Extension tiers in enums.md updated: starlark now implemented.

[0.29.1] — 2026-03-17

Summary

API extensions. Starlark packages serve custom JSON endpoints via api_routes manifest key, with SSRF-safe outbound HTTP, LLM provider access via BYOK chain, capability-based routing, and config-file provider types. Five changesets (CS0CS4).

New

  • Extension API routes (/s/:slug/api/*path) — Starlark packages declare routes in manifest.api_routes and implement on_request(req) entry point. Supports exact and prefix path matching, wildcard methods, and boolean shorthand. JWT-authenticated.
  • HTTP outbound module (http) — http.get/post/put/delete/request builtins with SSRF protection (private/loopback IP blocking via net.Dialer.Control), manifest-based allowlist/blocklist (network_access.allow/block), redirect chain validation, 1 MB response cap, 10 s timeout. Requires api.http permission.
  • Provider module (provider) — provider.complete(messages, model?, max_tokens?, temperature?) makes synchronous LLM calls via BYOK chain. requires_provider manifest key with optional provider_config_id pin and model default. Requires provider.complete permission.
  • RunContext — per-invocation state (user_id, channel_id) passed through CallEntryPoint for user-scoped modules. All callers updated: API routes pass JWT user, task executor passes owner, filters pass nil.
  • ProviderResolver interface — decouples sandbox from handlers package (avoids circular dependency). ProviderResolverAdapter in handlers bridges to ResolveProviderConfig + provider registry.
  • capability_match routing policy — filters candidates by model capabilities (required_caps: ["tool_calling", "vision"]) with optional prefer_cheapest sort by output price. Forward-compatible with unknown capability names.
  • Config-file provider typesPROVIDER_TYPES_FILE env var points to JSON array of custom provider type definitions. Registers OpenAI-compatible endpoints (Ollama, LiteLLM, vLLM) at startup without code deploy. Built-in IDs are protected from override.
  • provider.complete permission — new extension permission constant for LLM completion access from Starlark scripts.

Changed

  • Runner.ExecPackage and Runner.CallEntryPoint signatures gain *RunContext parameter. All callers updated (breaking internal API, no external impact).
  • Runner.buildModules receives manifest for module-specific config (http reads network_access, provider reads requires_provider).
  • Routing policy admin handler validation switch includes capability_match.
  • PolicyConfig gains RequiredCaps and PreferCheapest fields.
  • Routing Context gains Capabilities map for capability-match evaluation.

Deferred

  • Server-side tool execution in completion handler → v0.29.2. provider.complete() covers bare completions; full tool loop (extensions defining tools that execute server-side during chat completions) requires tool registry integration with the sandbox, which aligns with the DB extensions scope.

[0.29.0] — 2026-03-17

Summary

Starlark sandbox + permission model. Server-side extension runtime with pre-completion filter chain, permission-gated modules, and admin review workflow. Six changesets (CS0CS5) on top of Phase 0 store cleanup (12 changesets, CS0CS7b).

Phase 0 migrated ~242 raw SQL calls into the store interface layer, giving the Starlark sandbox a clean API surface. Phase 1 builds the runtime: sandboxed eval loop, permission model, secrets/notifications modules, task executor integration, and pre-completion filter chain with extension discovery.

DB rebuild required. Migration 016 rewritten in-place (adds status column to packages, extension_permissions table).

New

  • Pre-completion filter chain (server/filters/) — composable PreCompletionFilter interface with ordered execution, error isolation, and logging. Chain.Register() / Chain.Execute(). Built-in filters use order 099; extension filters use 100+.
  • KB auto-inject filterKBInjectFilter (order 10) replaces inline BuildKBHint(). Scans persona, project, channel, and personal KBs. Reference implementation for the filter model.
  • Starlark sandbox (server/sandbox/) — go.starlark.net interpreter with step limits (1M ops default), context timeout, captured print output, disabled load(). MakeModule() helper for exposing Go functions to Starlark scripts.
  • Sandbox runnerRunner.ExecPackage() loads script from manifest _starlark_script, assembles module set from granted permissions. CallEntryPoint() for event-driven invocation.
  • Permission modelextension_permissions table tracks declared capabilities from package manifests. Admin grant/revoke controls which modules the sandbox injects at runtime. Lifecycle: install → pending_review → grant-all → active, revoke → suspended. Auto-transitions on grant/revoke.
  • Extension permission constantssecrets.read, notifications.send, filters.pre_completion, db.read, db.write, api.http. Validated against manifest declarations.
  • Secrets modulesecrets.get(key) / secrets.list(). Per-package key-value store backed by GlobalConfig (ext_secrets:{packageID}). Admin CRUD endpoints.
  • Notifications modulenotifications.send(user_id, title, body?, type?). Wraps platform notification service. Extensions cannot send email or bypass user preferences.
  • Starlark filterStarlarkFilter bridges the filter chain to extension scripts. Calls on_pre_completion(ctx), parses return [{"role": "system", "content": "..."}].
  • Starlark filter discoveryDiscoverStarlarkFilters scans active packages with filters.pre_completion grant at startup.
  • task_type: "starlark" — executor executeStarlark loads package by system_function (package ID), calls on_run() entry point. Output to channel/webhook/run record.
  • Extension secrets adminGET/PUT/DELETE /admin/extensions/:id/secrets. GET returns keys only (not values).
  • Extension permission adminGET .../permissions, GET .../review, POST .../grant, POST .../revoke, POST .../grant-all.
  • ICD runner packaging tier — 18 tests: permission lifecycle (install → pending_review → grant → active → revoke → suspended), secrets CRUD, invalid permission rejection.

Changed

  • BuildKBHint deprecated — replaced by KBInjectFilter in the pre-completion filter chain. Kept for backward compatibility; removal scheduled for v0.30.0.
  • packages tablestatus column added (active, pending_review, suspended). PackageStore.SetStatus() method.
  • CompletionHandlerfilterChain field + SetFilterChain() setter. Filter chain runs between system prompts and message history.
  • Executorrunner field + SetRunner(). Starlark dispatch branch between action and prompt types.
  • AdminInstallExtension — calls SyncManifestPermissions() after package creation. Parses "permissions" from manifest.
  • Starlark task validationtask.starlark RBAC gate enforced. system_function required (holds package ID). Package must exist and be starlark tier.

Phase 0 — Store Cleanup

  • ~242 raw SQL calls eliminated — every handler, tool, and middleware file now goes through the store interface.
  • Constructor signature changesenforcePrivateProviderPolicy(ctx, stores, ...), ResolveProviderConfig(stores, vault, ...), NewChannelHandler(stores).
  • 30+ new store methods across NoteStore, MessageStore, FileStore, ProjectStore, TeamStore, ProviderStore, CatalogStore, GlobalConfigStore, AuditStore, ChannelStore.

Fixed

  • API._delete aliasdelete is a JS reserved word; added _delete as alias for _del on the API client.
  • safeString nil handling — returns Go nil, not "".
  • nil JSONMap → PG jsonbToolCalls/Metadata initialized to models.JSONMap{} before store calls.
  • GetByID vs GetParentAndRole — cursor update uses GetParentAndRole (matches original raw SQL deleted_at filter).

[0.28.8] — 2026-03-15

Summary

ICD Green Board. Close out all pre-existing ICD test failures before v0.29.0 feature work. Gate: 543/543 (100%) on the ICD runner.

Three changesets: provider sync + infrastructure resilience (cs0), CORS lockdown + WebSocket origin validation (cs1), WebSocket ticket exchange (cs2).

Fixed

  • Batched UpsertFromSync — catalog sync rewritten as a single transaction: one SELECT to identify existing models, N prepared INSERT ON CONFLICT DO UPDATE statements, one COMMIT. Previously each model was N×2 auto-committed queries (SELECT + INSERT/UPDATE), producing ~300 round-trips and WAL flushes on a typical Venice sync. Both PG and SQLite stores rewritten. Root cause of cascading 502s: the write burst starved the connection pool and caused Traefik to see stale keepalive connections on subsequent requests.
  • DB connection lifecycleSetConnMaxLifetime(5m) and SetConnMaxIdleTime(1m) added to the Postgres pool. Previously connections lived forever — stale TCP after PG restart or network blip would fail silently until a query hit the dead connection.
  • HTTP transport poolProviderConfig.Client() now returns a shared *http.Client from a sync.Map keyed by (ProxyMode, ProxyURL). Previously every outbound provider call allocated a fresh http.Transport, leaking idle connections until GC.
  • Traefik retry middleware — new Middleware CRD (2 attempts, 100ms initial interval) applied via ingress annotation. Retries on 502 and connection reset. Both k8s/ CI manifests and Helm chart templates updated. CI pipeline renders and applies the middleware before the ingress resource.
  • Provider sync timeoutsyncProviderModels now wraps the outbound provider HTTP call with a configurable timeout (default 30s, PROVIDER_SYNC_TIMEOUT env var). Previously used no timeout, causing Gin handler timeouts and cascading 502s from the reverse proxy when upstream providers (especially Venice) were slow.
  • 504 on upstream timeoutPOST /admin/models/fetch and POST /api-configs/:id/models/fetch now return 504 with structured {"error": "upstream timeout: <provider>"} instead of letting the reverse proxy manufacture a 502 from a broken connection.
  • ICD runner provider tier resiliencemodels/fetch calls retry once on 502/504 with 2-second backoff. Eliminates cascading test failures caused by transient upstream timeouts.
  • WebSocket CheckOriginupgrader.CheckOrigin now validates the Origin header against CORS_ALLOWED_ORIGINS instead of unconditionally returning true. Connections from unlisted origins are rejected at upgrade time.
  • CORS startup warning — if CORS_ALLOWED_ORIGINS is explicitly set to *, a warning is logged at startup recommending restriction for production deployments.

New

  • WebSocket ticket exchangePOST /api/v1/ws/ticket returns a single-use opaque ticket (128-bit random hex, 30s TTL). Client connects with ?ticket=<opaque> instead of ?token=<jwt>. Ticket is validated and deleted atomically (single-use). Background reaper removes expired unclaimed tickets every 15 seconds. This eliminates JWT exposure in server logs, proxy logs, and browser history.
  • WsAuth middleware — dedicated WebSocket auth middleware with three-path priority: ?ticket= (preferred), ?token= (legacy, logs deprecation warning), Authorization header (non-browser).
  • TicketStore — in-memory ticket store with Issue(), Validate(), Stop(), and automatic TTL reaping.
  • GetAllowedOrigins() — exported CORS origin resolver for use by WebSocket hub and other subsystems.
  • Traefik retry Middleware CRDk8s/middleware-retry.yaml and chart/templates/middleware-retry.yaml. Configurable via ingress.retry.attempts and ingress.retry.initialInterval in Helm values.

Changed

  • events.NewHub(bus, allowedOrigins) — Hub constructor now takes the resolved CORS origin string. The upgrader's CheckOrigin is configured once at construction time.
  • events.js _doConnect() — now async. Fetches a ticket via POST /api/v1/ws/ticket before building the WebSocket URL. Falls back to legacy ?token= if ticket exchange fails (pre-v0.28.8 server compatibility).
  • WebSocket routeGET /ws now uses WsAuth middleware instead of generic Auth. Legacy ?token= still works but logs deprecation.
  • ProviderConfig.Client() — returns shared client from pool instead of allocating per call.

Docs

  • docs/ICD/websocket.md — ticket exchange protocol documented (preferred auth, legacy deprecation, origin validation).
  • docs/ARCHITECTURE.md — expanded CORS configuration section (environment behavior, production guidance, WebSocket interaction).
  • docs/ROADMAP.md — v0.28.x Tier 2 bucket eliminated (items relocated to pinned versions: v0.29.1, v0.30.0, v0.30.1). Pre-1.0 Code Quality Sweeps section deleted (relocated to v0.29.0 Phase 0 with CI-enforced gate). Zero unscheduled items remain outside TBD.

[0.28.7] — 2026-03-15

Summary

Unified packaging. The surface_registry and extensions tables are replaced by a single packages table. Surfaces, extensions, and combined "full" packages install through one endpoint (POST /admin/packages/install) with one archive format (.pkg). Task RBAC pre-positions the task.starlark permission gate for v0.29.0.

DB rebuild required. Migrations 012 and 016 are rewritten in-place. Extension surfaces must be re-installed after upgrade. Builtin extensions and core surfaces re-seed automatically.

New

  • Unified .pkg archive format — zip containing manifest.json + static assets (js/, css/, assets/). Manifest type field determines behavior: surface (routable page), extension (hooks/tools/pipes), full (both). Missing type defaults to surface for backward compatibility with .surface archives.
  • packages table — replaces surface_registry + extensions. Text slug PKs, unified schema with type/version/tier/scope columns. package_user_settings replaces extension_user_settings.
  • POST /admin/packages/install — unified install endpoint. Validates type-specific requirements (extensions need tools/pipes/hooks, full packages need both route and extension behavior). Accepts .pkg, .surface, .zip.
  • Admin packages section — replaces separate Surfaces + Extensions sections. Type badges, filter by type, install upload area.
  • /admin/surfaces/* aliases — all surface admin endpoints retained as aliases pointing to the packages handlers. No URL-breaking change.
  • task.starlark permission — pre-positioned in AllPermissions. Handler rejects task_type: "starlark" with 400 referencing v0.29.0. Not granted to any group, not exposed in admin UI until v0.29.0.

Changed

  • SeedBuiltinExtensionsSeedBuiltinPackages — writes to packages table with source='builtin', type='extension', is_system=true. Same idempotent version-aware logic.
  • Stores.Surfaces + Stores.ExtensionsStores.Packages — single PackageStore interface (15 methods). Compiler-enforced migration across all handlers, pages, and completion pipeline.
  • Static asset disk path/data/surfaces//data/packages/. URL path /surfaces/:id/*path retained for stability.
  • Extension runtimeListForUser, GetUserSettings, SetUserSettings, ServeExtensionAsset, ListBrowserToolSchemas all read from packages table. Same API contract, new backing store.

Removed

  • extensions table (migration 012) — replaced by packages.
  • surface_registry table (migration 016) — replaced by packages.
  • extension_user_settings table — replaced by package_user_settings.
  • ExtensionStore interface — subsumed by PackageStore.
  • SurfaceRegistryStore interface — subsumed by PackageStore.
  • surfaces/ repo directory — renamed to packages/.
  • admin-surfaces.js — replaced by admin-packages.js.

[0.28.6] — 2026-03-15

Summary

Infrastructure release. Virtual scroll for long conversations, Helm chart for Kubernetes deployment, system task type (Go function registry replacing ad-hoc goroutines), admin broadcast notifications, server-side SSH key generation for git credentials, task webhook trigger UI, and model preference dedup fix.

KB auto-inject pipe filter deferred to v0.29.0 — will be built as the reference Go implementation of the server-side filter model alongside Starlark, ensuring the architecture is right and performant before user code runs in it.

New

  • Virtual scroll — viewport-windowed message rendering for long conversations (100+ messages). IntersectionObserver-based sentinel triggers prepend chunks of 40 as user scrolls up. DOM capped at ~80 nodes regardless of conversation length. Streaming messages always rendered. Transparent fallback for short conversations.
  • Helm chartchart/ directory with full Kubernetes deployment. helm install switchboard ./chart replaces raw manifests. Configures backend + frontend deployments, services, ingress (Traefik), PVC, ConfigMap, Secret. All config.go env vars exposed as values.
  • System task typetask_type: "system" with Go function registry. Built-in functions: session_cleanup, staleness_check, retention_sweep, health_prune. Admin creates task, picks function from dropdown, sets cron schedule. Executor calls registered Go function instead of LLM. Replaces ad-hoc goroutine background jobs with visible, configurable, auditable tasks. Permanent track — not replaced by Starlark. GET /admin/system-functions returns available functions.
  • Admin broadcastPOST /admin/notifications/broadcast sends system.announcement notification to all active users via NotifyMany. Emits system.broadcast WebSocket event. Admin UI compose form with title, message, level selector (info/warning/critical).
  • Git credential key generationPOST /git-credentials/generate creates ED25519 SSH keypair server-side. Private key vault-encrypted, never exposed. Public key + SHA256 fingerprint returned for user to add to git host. Optional persona_id for per-persona commit attribution. GET /git-credentials/:id/public-key for clipboard copy. Settings UI section with generate, list, copy, delete.
  • Task webhook trigger UI — schedule selector gains "Webhook trigger" option. Trigger URL displayed with copy button after creation. Link button on webhook tasks in list view. webhook_secret field for HMAC signing. Task-to-task chaining documented (Task A webhook_url → Task B trigger URL).

Fixed

  • Model preferences NULL-in-UNIQUEprovider_config_id column on user_model_settings changed to NOT NULL. The UNIQUE(user_id, model_id, provider_config_id) constraint silently allowed duplicates when the column was NULL. Handler already enforced binding:"required" — schema now matches.
  • ICD runner shapeS.modelPreference gains provider_config_id field (smoke tier shape assertion).

Changed

  • ListActiveUserIDs added to UserStore interface (PG + SQLite).
  • NotifTypeAnnouncement constant added to notification types.
  • Task model gains system_function field. Task stores updated (taskColumns, scanTask, Create) in both PG and SQLite.
  • ICD runner version bumped. New tests: model prefs (8), broadcast (3), git credentials (6), system tasks (4).

Docs

  • notifications.md — broadcast endpoint, system.broadcast WS event.
  • admin.md — broadcast cross-reference.
  • workspaces.md/generate, /public-key endpoints, new fields.
  • tasks.md — system task type, function registry, chaining pattern, GET /admin/system-functions.

[0.28.5] — 2026-03-14

Summary

Frontend SDK + pipe/filter pipeline. switchboard-sdk.js provides a single-object composition layer (sw) over the 15+ platform globals. Three-stage pipe/filter pipeline (pre-send, post-receive stream, post-render) formalizes the ad-hoc extension hook model into composable, priority-ordered, scope-aware transform chains. Backward-compatible compat shim for existing ctx.renderers.register() extensions. ICD runner gains sdk test tier (36 tests).

New

  • switchboard-sdk.js — SDK core. Switchboard.init() returns sw singleton with identity (sw.user, sw.isAdmin), REST client (sw.api.get/post/put/del/stream), events (sw.on/once/off/emit), theme (sw.theme.current/mode/set/on), UI primitives (sw.toast/confirm/modal), and component wrappers (sw.chat()).
  • Pipe/filter pipeline — three composable stages:
    • sw.pipe.pre(priority, fn, opts) — pre-send: transform user message before LLM request. Runs on both sendMessage() and regenerateMessage(). Context includes regenerate flag and regenerateMessageId.
    • sw.pipe.stream(priority, fn, opts) — post-receive: transform each SSE chunk during streaming. Sync-only for performance.
    • sw.pipe.render(priority, fn, opts) — post-render: transform rendered DOM after DOMPurify. Replaces runExtensionPostRender() delegation.
  • Filter scoping — filters declare optional scope: { channelType: [...] } to restrict execution to specific channel types. Scoped-out filters are skipped with zero overhead.
  • Pipeline introspectionsw.pipe.list() returns all registered filters by stage with priority, source, scope, call count, avgMs, errors.
  • Extension compat shimctx.renderers.register(name, { type: 'post' }) auto-shims into sw.pipe.render(). Existing block renderers (mermaid, katex, csv, diff) continue working through the unchanged formatMessage() path. Shimmed renderers appear in sw.pipe.list().
  • ICD runner sdk tier — 36 tests: boot, identity, REST client, events, theme, pipe registration, execution ordering, scoping, halt semantics, error isolation, compat shim, introspection.

Changed

  • base.html — SDK script tag added after component scripts. Universal init block simplified to Switchboard.init() call. UserMenu band-aid (cs15) absorbed into SDK _hydrateUserMenu().
  • app.jsstartApp() calls Switchboard.init() (idempotent).
  • chat.jssendMessage() and regenerateMessage() run pre-send pipe. New _buildPreSendContext() helper.
  • ui-core.jsstreamResponse() SSE loop runs stream pipe per chunk.
  • ui-format.jsrunExtensionPostRender() delegates to render pipe with fallback to Extensions.runPostRenderers().
  • extensions.js_registerRenderer() shims type: 'post' renderers into sw.pipe.render().
  • ICD runner version bumped to 0.28.5.0, sdk tier added.

[0.28.4] — 2026-03-14

Summary

Security tier red team. 58 adversarial tests added to the ICD runner exposing 5 bugs (2× P0, 2× P1, 1× P2), all fixed in this release. Auth middleware rewritten with a user-status cache so deactivation and role changes take effect within 30 seconds. surfaces/ directory added to the repo with a build script for packaging extension surfaces.

Security Fixes

  • [P0] Deactivated user JWT accepted. Auth middleware validated signature + expiry only — never checked is_active in the database. A deactivated user's existing JWT continued working until natural expiry (15 min). Fix: middleware/auth.go now performs a cached DB lookup (UserStatusCache, 30 s TTL) on every request. Deactivated users receive 401 within seconds.
  • [P0] Demoted admin JWT retained admin access. The role context value was read from JWT claims, not the database. Demoting an admin via PUT /admin/users/:id/role had no effect until the user's token expired. Fix: role is now resolved from the DB on every request via the same cached lookup. RequireAdmin() reads the live role.
  • [P1] Null byte in path parameter → 500. Gin passed %00 through to the database layer, causing a scan error. Fix: new ValidatePathParams() middleware rejects null bytes with 400.
  • [P1] 4096-char path parameter → 500. Same root cause — oversized params reached the DB. Fix: ValidatePathParams() rejects params longer than 255 characters with 400.
  • [P2] CORS Access-Control-Allow-Origin: * in all environments. Fix: production mode now defaults to same-origin only. Override with CORS_ALLOWED_ORIGINS=https://app.example.com (comma-separated). Development mode retains *.

Added

  • ICD Security Tier — 58 adversarial tests across 6 domains: auth-boundary (8), cross-tenant (20), input-validation (18), session (6), escalation (7), transport (3). Red "Security" button in the runner UI. Requires fixtures.
  • UserStatusCache (middleware/auth.go) — per-user {isActive, role, fetchedAt} cache with 30 s TTL. Shared by Auth(), AuthOrSession(), and AuthOrRedirect(). Cache eviction on UpdateUserRole and ToggleUserActive via callback.
  • ValidatePathParams() middleware (middleware/validate.go) — rejects null bytes and >255-char path params before they reach handlers.
  • surfaces/ directory — source tree for extension surfaces with build.sh that packages each subdirectory into dist/<n>.surface. hello-dashboard and icd-test-runner moved from loose archives into version-controlled source.
  • surfaces/README.md — structure docs, build instructions, conventions.
  • ICD runner: safeDelete() now retries 2× with 500 ms backoff on 502/503, swallows cleanup errors with console.warn instead of throwing.
  • ICD runner: assertDenied() / assertRawDenied() helpers treat 502 as INCONCLUSIVE instead of CRITICAL.

Changed

  • middleware.Auth() signature: (cfg)(cfg, users, cache).
  • middleware.AuthOrSession() signature: (cfg, stores)(cfg, stores, cache).
  • middleware.AuthOrRedirect() signature: (cfg)(cfg, users, cache).
  • middleware.CORS() signature: ()(cfg).
  • AdminHandler gains OnUserChanged(func(userID string)) — wired to cache.Evict in main.go.
  • 10 test files updated for new middleware signatures.
  • .gitignore: added *.surface under build artifacts.
  • ICD runner manifest bumped to 0.28.4.0.

Removed

  • hello-dashboard.surface at repo root (replaced by surfaces/hello-dashboard/ source directory).

Verified (not bugs)

  • Cross-tenant isolation: 20/20 — notes, channels, messages, tasks, workspaces, projects, BYOK configs, memories, team providers/ personas/members all properly isolated.
  • SQL injection: 7 payloads across 3 search endpoints — all parameterized.
  • XSS storage: 3 round-trip tests — no server crashes (frontend sanitizes via DOMPurify).
  • Escalation: self-promote, cross-role create, team-admin → platform-admin, user_id substitution — all blocked.
  • JWT tampering + alg=none: both rejected.
  • Refresh token rotation + revocation: both working.
  • Auth rate limiting: 429 after burst.
  • Unauthenticated access: all 6 endpoints return 401.

Known

  • [P2] WebSocket ?token= query parameter exposes JWT in server logs and browser history. Informational — consider ticket exchange pattern post-1.0.
  • Cross-visitor session isolation: enforced by session_auth.go channel binding, but no E2E test yet (requires public_link workflow setup — defer to visitor E2E milestone).

[0.28.3.5] — 2026-03-14

Summary

Frontend decomposition Phase 4: ES module conversion. IIFE wrappers removed from all 47 JS files, <script type="module"> applied across all templates. sb.js and vendor libs stay as classic scripts to guarantee availability before modules execute.

Changed

Phase 4 — IIFE Removal

  • Removed (function() { 'use strict'; ... })(); wrappers from 45 standard-pattern JS files (3 lines per file: open, strict, close).
  • Removed 'use strict'; from 2 arrow-IIFE files (knowledge-ui.js, tools-toggle.js) — arrow IIFE retained (const binding pattern, harmless in module scope).
  • Removed leftover 'use strict'; from persona-kb.js (not caught by the main script due to blank line before directive).
  • Inner IIFEs preserved: projects-ui.js (visibility timer), ui-primitives.js (Providers/Roles factory constructors).
  • All 48 JS files pass node --check syntax validation.

Phase 4 — Template Module Conversion

  • All <script src=".../js/*.js"> tags converted to <script type="module" src="..."> across 6 template files: base.html, surfaces/chat.html, surfaces/admin.html, surfaces/editor.html, surfaces/notes.html, surfaces/settings.html.
  • Classic scripts retained for: sb.js (must be globally available before any module), vendor libs (marked.min.js, purify.min.js, codemirror.bundle.js), early theme script, __BASE__/__VERSION__ hydration.
  • 3 inline scripts in base.html converted to type="module": API.loadTokens(), Theme/appearance init + handleLogout fallback, UserMenu hydration.
  • handleLogout fallback: added sb.register('handleLogout', handleLogout) since function is now module-scoped (no longer on window).
  • UserMenu hydration: handleLogout()sb.call('handleLogout'), openDebugModal()sb.call('openDebugModal').
  • Extension surface main.jstype="module".
  • DOMContentLoaded inline scripts in surface templates stay classic (safe — modules execute before DOMContentLoaded fires).

Fixed

  • CI: frontend test harness (helpers.js) now loads sb.js into VM context before other source files. Fixes 55 extension test failures (ReferenceError: sb is not defined at events.js:334).
  • extensions.test.js, extensions-builtin.test.js: loadExtensions() prepends sb.js load. Result: 219/219 tests pass.

Not Changed

  • sb.register()/sb.ns() dual-write to window[name] stays — cross-file JS references (API.listProjects(), UI.toast()) still go through window.*. Removal requires import/export statements (Phase 5, future).
  • login.html, workflow.html, workflow-landing.html unchanged (standalone pages, no sb.js).

[0.28.3.3] — 2026-03-14

Summary

CI fix: frontend test harness missing sb.js in VM context. All 55 extension test failures were the same root cause — events.js calls sb.ns('Events', Events) but the test VM never loaded sb.js.

Fixed

  • helpers.js: loadAppModules() now loads sb.js before app-state.js (defensive — not currently broken but would fail if any passing test switched to using loadAppModules with source files that call sb.*).
  • extensions.test.js: loadExtensions() loads sb.js before events.js.
  • extensions-builtin.test.js: same fix.

Result: 219/219 tests pass (was 164/219).

[0.28.3.2] — 2026-03-14

Summary

Frontend decomposition Phase 3b: Go template onclicksb.call() migration. All server-rendered onclick handlers in templates that load sb.js (via base.html) now route through the action registry. This clears the template gate for Phase 4 (ES module conversion).

Changed

Phase 3b — Go Template onclick → sb.call()

  • Converted 72 onclick="fn(args)" handlers across 10 Go template files to use sb.call('fn', args) or sb.callEvent(event, 'fn', args).
  • Templates converted: base.html (10), surfaces/chat.html (28), surfaces/admin.html (14), surfaces/settings.html (3), admin/providers.html (5), admin/users.html (3), admin/roles.html (1), admin/routing.html (3), admin/teams.html (4), admin/settings.html (1).
  • 4 onclick handlers intentionally left unconverted (inline DOM manipulation or Go template variable in element ID):
    • this.parentElement.style.display='none' (crash banner dismiss)
    • event.stopPropagation() (bare container click guard)
    • document.getElementById('settingsTeamAddMember').style.display='none'
    • document.getElementById('{{.FieldName}}Input').click()
  • 3 standalone pages excluded (no sb.js): login.html, workflow.html, workflow-landing.html.
  • Defensive typeof guards in settings.html onclick handlers (e.g. if(typeof UI!=='undefined')UI.saveAppearance?.()) replaced with clean sb.call() — registry silently logs unresolved actions.
  • sb.js comments updated: template gate cleared, dual-write removal deferred to Phase 4 (cross-file JS references still use window.*).

[0.28.3.1] — 2026-03-14

Summary

Frontend decomposition Phases 2 + 3. Phase 2: onclick→data-action delegation migration (143 → 5 unconvertible). Phase 3: sb.js action registry replacing all window.* exports with centralized sb.register()/sb.ns() dispatch.

Added

Action Registry — sb.js (Phase 3)

  • New file src/js/sb.js, loaded first in base.html.
  • sb.register(name, fn) — register standalone action, dual-writes to window[name] for backward compat until ES modules land.
  • sb.ns(name, obj) — register namespace object (API, UI, App, etc.), methods accessible via dot notation: sb.resolve('UI.copyMessage').
  • sb.resolve(name) — centralized action resolution for flat names and dot-notation namespace methods. Falls back to window[name].
  • sb.call(name, ...args) — template bridge for Go onclick handlers.
  • sb.callEvent(event, name, ...args) — event passthrough variant.
  • sb.list() — introspection: list all registered actions/namespaces.
  • sb.has(name) — check if action is registered.

Changed

Phase 2 — onclick→data-action Delegation

  • 138 of 143 dynamic inline onclick handlers converted to data-action
    • data-args attributes with centralized event delegation.
  • _uiDispatch(container): idempotent delegated click handler wired on document.body. Supports data-args (JSON), data-pass-event (context menu positioning), data-pass-el (checkbox state).
  • _ctxDispatch(container): idempotent delegated handler for context menus and panel buttons (auto-dismisses menu after action).
  • Wrapper functions for compound expressions: _toggleArchivedProjects, _submitEditFromDOM, _copyCodeBlock, _openNoteFromTool, _toggleNoteSelectCb, _noteItemClick.
  • 5 unconvertible onclick remain (self-referencing DOM removal, computed onclick from variable).

Files converted (onclick): projects-ui.js (26→0), ui-core.js (25→1), ui-admin.js (28→1), ui-format.js (10→0), notifications.js (8→0), notes.js (8→0), channel-models.js (5→0), ui-settings.js (8→0), files.js (3→0), admin-handlers.js (2→0), app.js (4→1), workflow-queue.js (6→0), workflow-admin.js (5→0), task-settings.js (1→0), task-sidebar.js (3→1).

Phase 3 — Action Registry Migration

  • 234 top-level window.X = X exports across 35 files converted to sb.register('X', X) or sb.ns('X', X). 28 namespace objects, 206 standalone functions.
  • 14 in-body window.X = function assignments across 6 files (settings-handlers.js, task-admin.js, task-settings.js, task-sidebar.js, workflow-admin.js, workflow-queue.js) converted to sb.register('X', function) or local const + sb.ns().
  • _uiDispatch and _ctxDispatch dispatch updated to use sb.resolve() instead of window[] walking.
  • base.html loads sb.js before app-state.js.
  • sb.register/sb.ns dual-writes to window.* for backward compatibility — this side-effect is removed when files convert to ES module imports (Phase 4).

Metrics

v0.28.3 v0.28.3.1
Dynamic inline onclick 143 5 (unconvertible)
data-action delegated 0 163
window.* exports 248 1 (debug fetch patch)
sb.register calls 0 220
sb.ns calls 0 30
Action resolution window[] sb.resolve() centralized

[0.28.3] — 2026-03-13

Summary

ICD audit close-out and straggler sweep. Rolls in the remaining v0.28.2 items (websocket doc, auth/enums reconciliation) since the 0.28.3 branch was cut before the final v0.28.2 changeset landed.

Changed

WebSocket ICD (websocket.md) — Full Rewrite

  • Event envelope field corrected: "event" not "type" (matches Go struct json tag json:"event").
  • Room subscribe/unsubscribe documented as planned, not implemented. JoinRoom/LeaveRoom exist as stubs but no client wiring exists; all delivery uses Hub.SendToUser().
  • Added payload shape documentation for 11 event types: message.created (user + assistant variants), typing.start, typing.stop, typing.user, user.presence, user.mentioned, notification.new, notification.read, workflow.assigned, workflow.claimed, workflow.advanced, workflow.completed, role.fallback, tool.call.*.
  • Routing table rewritten with accurate Direction, Delivery mechanism, and labels matching events.routeTable in code.
  • Noted that workflow.claimed, workflow.advanced, workflow.completed use room-scoped Bus.Publish() which does not reach WebSocket clients (rooms not wired). Forward-looking plumbing only.

Auth ICD (auth.md)

  • Login/Register response shape corrected: added token_type ("Bearer") and expires_in (900), removed phantom fields (avatar_url, created_at, last_login) that generateTokens does not emit.
  • Register documents the admin-approval path (201 with message when is_active is false).

Enums ICD (enums.md)

  • Task run statuses: added missing queued status (present in DB CHECK constraint, was omitted from enum doc).

Presence Status Reconciliation

  • DB CHECK constraint: online, away, offline.
  • Runtime behavior: hub emits only online and offline. away is reserved for future idle detection.
  • Documented this gap in both websocket.md and enums.md rather than adding dead code or removing the DB constraint.

Housekeeping

  • VERSION bumped to 0.28.3.
  • CHANGELOG.md updated with entries for 0.28.0 through 0.28.3.
  • ROADMAP.md updated: v0.28.2 marked complete, v0.28.3 scope adjusted.

ICD Runner

No runner changes — websocket events are not testable via HTTP. All existing tests remain at 469/469 (100%).

Fixed

WebSocket Workflow Event Delivery (cs1)

  • workflow.advanced, workflow.completed, workflow.claimed were emitted via room-scoped Bus.Publish(), but the room subscription system is not wired (no client-side subscribe/unsubscribe). These events never reached WebSocket clients.
  • Converted all three to SendToUser() per channel participant, matching the pattern used by message.created and workflow.assigned.
  • workflow.claimed payload gains channel_id field (needed to look up participants; also useful for frontend navigation).
  • workflow_assignments.go: consolidated the duplicate channel_id query (was queried separately for WS and notification).

Added

JS Dependency Audit (cs2)

  • docs/JS-DEPENDENCY-AUDIT.md: full map of the frontend codebase. 47 files, 25K lines, 431 top-level globals, 143 dynamic onclick handlers. Documents the complete script load order per surface, core global dependency graph (App → 20 files, API → 36 files, UI → 32 files, Events → 9 files), IIFE vs bare global classification, inline handler inventory, cross-file call graph, and a 4-phase decomposition strategy (core extract → onclick migration → ES modules → template handler shim).

Core Four IIFE Extraction (cs3)

  • api.js: Wrapped in IIFE. BASE and _storageKey are now private (were leaking as implicit globals). Single export: window.API.
  • events.js: Wrapped in IIFE. Replaced _storageKey localStorage coupling with API.accessToken (eliminates cross-file private dependency). Single export: window.Events.
  • app-state.js: Wrapped in IIFE. resolveCapabilities() now private. Exports: window.App, window.fetchModels.
  • ui-primitives.js: Wrapped in IIFE. updateTabArrows() now private (zero external callers). 17 explicit window.* exports with manifest comment. Previously all 18 declarations leaked as implicit globals.

Tier 2 IIFE Extraction (cs4)

  • ui-format.js: Wrapped in IIFE. 11 explicit exports (7 cross-file + 4 onclick handlers), 9 privatized (_highlightMentions, _formatMarked, _formatBasic, _unwrapMarkdownFence, _decodeHTML, _looksLikeHTML, _hasPreviewContent, _extractLastHTMLBlock, _livePreviewTimer).
  • ui-core.js: Wrapped in IIFE. 3 exports (UI, renderPersonaForm, toggleSummarizedHistory), 3 privatized (avatarHTML, assistantAvatarURI, _isSummaryMessage).
  • pages.js: Wrapped in IIFE. 2 exports (Pages, _val), 6 privatized (_pageData, _show, _hide, _toast, _parseJSON, _api).

Tier 3 IIFE Extraction — Leaf Modules (cs5)

  • 11 files wrapped: channel-models.js, chat-pane.js, notifications.js, model-selector.js, file-tree.js, tokens.js, code-editor.js, note-editor.js, drag-resize.js, pane-container.js, user-menu.js.
  • 13 total explicit exports across all 11 files.
  • 17 functions privatized (e.g. _shortName, _timeAgo, _ftFileIcon, _ceFileIcon, _ceDetectLanguage, dismissContextWarning, _clientPos, 10 PaneContainer internals).

Small File IIFE Wrap (cs6)

  • 7 files: admin-surfaces.js, extensions.js, memory-ui.js, notification-prefs.js, persona-kb.js, repl.js, ui-settings.js.
  • ui-settings.js has no standalone exports (extends UI via Object.assign); IIFE scopes its closure only.
  • 3 functions privatized (_loadSurfaceList, _debounce, savePersonaKBs — zero external callers).

Chat + App Entry Point IIFE Wrap (cs7)

  • chat.js: Wrapped in IIFE. 22 explicit exports (ChatInput + 21 functions), 7 privatized (_typingTimer, _emitTyping, _saveChatModel, _restoreChatModel, _cleanStaleChatModel, updateChatTokenCount, _autoNamePending).
  • app.js: Wrapped in IIFE. 6 exports (handleLogin, handleRegister, handleLogout, switchAuthTab, startApp, initBanners), 13 privatized. init() self-invokes via DOMContentLoaded — not exported. handleLogout overrides the base.html fallback on chat, editor, and notes surfaces.

Clean Tier 4 Wraps (cs8)

  • settings-handlers.js: Wrapped in IIFE. 11 explicit exports + 4 self-assigned window.* functions (preserved from original). 13 privatized.
  • note-graph.js: Wrapped in IIFE. 6 exports, 17 privatized.
  • debug.js: Wrapped in IIFE. 8 exports (all onclick-referenced from base.html template), 1 privatized.
  • panels.js: Wrapped in IIFE. 6 exports, 4 privatized.

Heavy Page IIFE Wraps — All Remaining (cs9)

  • files.js: 18 exports (15 cross-file + 3 onclick), 18 privatized.
  • admin-handlers.js: 30 exports (28 cross-file + 2 onclick), 10 privatized. Previously misclassified as IIFE — was actually bare globals.
  • notes.js: 12 exports (9 cross-file + 3 onclick), 31 privatized.
  • ui-admin.js: 3 exports (ADMIN_SECTIONS, ADMIN_LABELS, ADMIN_LOADERS), 1 privatized. Also extends UI via Object.assign (28 onclick handlers are UI methods, not standalone).
  • projects-ui.js: 48 exports (29 cross-file + 19 onclick), 30 privatized. Largest file in the codebase (78 top-level globals reduced to 48 explicit exports).

Frontend decomposition Phase 1 complete. All 47 JS files are now IIFE-wrapped with explicit window.* exports. Zero implicit globals remain. The _storageKey cross-file coupling between api.js and events.js is eliminated. ~168 functions privatized across the codebase.

[0.28.2] — 2026-03-13

Summary

Full ICD audit across all remaining domains. Every ICD document traced against Go handlers, stores (PG + SQLite), and the ICD test runner. Eight changesets (cs0cs7), 469/469 runner tests passing.

Added

Go Integration Tests

  • projects_test.go: 14 tests — CRUD shapes, 201 status, envelope, admin list, auth, isolation.
  • workspace_test.go: 11 tests — list empty/data envelope, shape, root_path not exposed, user isolation, GET by ID, 404, 403, git credentials empty envelope, auth required.
  • profile_test.go: GET profile shape, PUT profile, duplicate email 409, settings envelope, password change (success, wrong current 401, too short 400), avatar delete.
  • Knowledge tests: document status polling, document delete, update empty body 400, permission denial.

ICD Runner Tests (v0.28.2.4)

  • Projects: rewritten from 9 → 19 tests (full CRUD shapes, channel/KB/note association lifecycles, files shape, isolation, admin list).
  • Knowledge: document status, document delete, permission, team-scoped KB creation.
  • Profile: all 7 endpoints exercised.
  • Workspaces: envelope, shape, isolation, git operations.
  • Notifications: envelope, preferences, type enum.

Fixed

P0 — Security

  • SetDiscoverable on knowledge bases had no authorization check. Added loadAndAuthorize + owner/admin gate + audit log (cs2).

P0 — Response Shape

  • GET /settings returned bare object → wrapped in {"settings": {...}} (cs5).
  • GET /workspaces returned bare array → {"data": [...]} (cs6).
  • GET /git-credentials returned bare array → {"data": [...]} (cs6).
  • GET .../git/log returned bare array → {"data": [...]} + nil slice guard (cs6).
  • ListByProject files returned {"files": null}{"files": []} (cs7).
  • GET /notifications/preferences missing "preferences" key in envelope (cs0).

P0 — Data Integrity

  • ListTeamProviderModels response struct missing Type field — embedding models indistinguishable from chat models, causing Venice 400 on completion (cs7).

Notification System

  • Implemented memory.extracted notification (was missing hook in memory extractor) (cs0).
  • Implemented user.mentioned persisted notification (was WS-only) (cs0).
  • Implemented workflow.claimed persisted notification (was WS-only) (cs0).
  • Removed dead NotifTypeProjectInvite constant (cs0).

ICD Runner (v0.28.2.4)

  • SSE parser: extract content from OpenAI-format choices[0].delta.content — was checking top-level evt.content. All three provider tiers (global, team, BYOK) were affected (cs7).
  • pickCheapestChat excludes embedding/image models by type and ID pattern (cs7).
  • Model ID references use (m.model_id || m.id) fallback for team provider models (cs7).

Changed

ICD Documents Corrected

  • notifications.md: object shape, query params, response envelopes, WS events. Type enum synced (removed aspirational, added implemented).
  • knowledge.md: KB object shape (6 field mismatches), search envelope (data not results), search result fields, status progression (extracting step), file type support, auth annotations.
  • profile.md: avatar API (multipart → JSON base64), response shapes for all 7 endpoints, auth annotations, field table. Added last_login_at to profileResponse + dialect-safe time scan.
  • workspaces.md: workspace object shape (indexing_enabled, git_*, total_bytes not storage_bytes, owner_type full enum), file entry shape, git status/log/commit shapes, archive format, auth annotations.
  • projects.md: full rewrite — 6 categories of drift fixed (ghost fields removed, 6 missing fields added, create/update shapes corrected, association objects documented, omitempty on computed counts noted).
  • notes.md: verified clean.
  • memory.md: verified clean (audited v0.28.0).

Dead Code Removal

  • ListGlobal/ListForTeam on KnowledgeBaseStore — interface + PG + SQLite (cs4).
  • CreateKB moved team role check from raw database.DB.QueryRow to stores.Teams.IsTeamAdmin; removed database import (cs4).
  • Stale /avatar route aliases removed from integration test harness (cs5).
  • ListDiscoverableKBs normalized to use toKBResponse() (cs3).

[0.28.1] — 2026-03-12

Summary

Surfaces ICD audit — first domain audit pass. Corrected 6 ICD discrepancies and hardened the surface install path.

Fixed

  • ICD surfaces.md corrected: field name, archive format, response shape (6 discrepancies).
  • Surface ID slug validation + extractableRelPath install hardening (path traversal defense).

Added

  • 19 E2E surface CRUD tests in ICD runner (install, enable/disable, delete, error paths).
  • 22 handler-level tests + 14 store-level tests (PG + SQLite).

Changed

  • CI timeout increased 8m → 12m for PG integration tests.

[0.28.0] — 2026-03-12

Summary

Platform polish arc kickoff. Memory ICD audit — first domain to complete the full trace-and-test cycle.

Changed

  • memory.md ICD audited and verified: object shape, scopes (user/persona/persona_user), statuses (active/pending_review/archived), all endpoints, admin review, AI tools, background extraction, memory injection.
  • 13/13 ICD runner tests passing.

[0.27.5] — 2026-03-11

Summary

Team tasks — team members can view team-scoped tasks, team admins can create and manage them. Settings and sidebar surfaces show team tasks alongside personal tasks with team attribution badges.

Added

Team Task Routes

  • GET /api/v1/teams/:teamId/tasks — list team tasks (any team member).
  • POST /api/v1/teams/:teamId/tasks — create team-scoped task (team admin).
  • PUT/DELETE/run/kill on team-scoped task routes (team admin).

Access Control Helpers

  • canAccessTask() — owner, system admin, or team member for team tasks.
  • canMutateTask() — owner, system admin, or team admin for team tasks.
  • Both used across Get, Update, Delete, ListRuns, RunNow, KillRun.

Frontend

  • Settings Tasks section fetches team tasks via /teams/mine + per-team /teams/:id/tasks. Shows team badge on team-scoped tasks.
  • Sidebar Tasks section includes active team tasks with [TeamName] label.

Fixed

  • ListRuns handler now checks task access (was unauthenticated — any user with a task ID could read run history).

[0.27.4] — 2026-03-11

Summary

Personal tasks — the user-facing task experience. Settings surface gains a Tasks section with CRUD, schedule builder (presets + custom cron + timezone), and 5 starter templates. Chat sidebar gains a Tasks section showing active tasks with status indicators and run-now buttons. Executor gains note output mode and BYOK provider enforcement.

Added

Settings → Tasks Section (task-settings.js)

  • Task list with name, schedule, timezone, last/next run, status badges. Pause/resume toggle, run-now, and delete buttons per task.
  • Create task form: name, model, prompt editor, schedule builder (6 presets + custom cron), timezone (defaults to browser), output mode (channel/note/webhook), budget overrides, notification toggles.
  • 5 starter templates: Morning News Digest, Daily Standup Prep, Weekly Project Summary, Stock Watchlist Check, Research Digest. Click to pre-populate the create form.

Tasks Sidebar Section (task-sidebar.js)

  • New collapsible sidebar section (after Workflows/Queue).
  • Shows active tasks with status indicators: ✓ (last run succeeded), ◷ (scheduled). Click opens the task's output channel.
  • Per-task ▶ run-now button.
  • Auto-refreshes on chat list update.

BYOK Provider Enforcement

  • tasks.personal_require_byok global config key (default: false). When true, personal-scope tasks fail with a clear error if no BYOK provider is available — prevents fallthrough to org providers.
  • Admin configuration UI in admin Tasks → Configuration tab.

Note Output Mode

  • output_mode: "note" now functional. Task executor creates a note (title: "{task name} — {timestamp}", tags: ["task-output"]) instead of a message in the service channel. Good for structured reports.

Changed

  • taskutil.TaskConfig gains PersonalRequireBYOK field.
  • executor.go persistence section rewritten as output_mode switch (channel/note/webhook).
  • task-admin.js config tab gains "Require BYOK for Personal" toggle.
  • ui-core.js renderChatList refreshes TaskSidebar alongside WorkflowQueue.
  • settings.html gains Tasks nav link and panel mount point.
  • chat.html includes task-sidebar.js.

[0.27.3] — 2026-03-10

Summary

Task chaining — webhooks for external integration, AI-spawnable sub-tasks, and workflow-to-workflow completion triggers. Also fixes a bug where the workflow_advance tool didn't fire on_complete chains.

Added

Webhook Delivery (server/webhook/)

  • webhook.Deliver(url, secret, payload) — POST with HMAC-SHA256 signing. 3 retries with exponential backoff (1s, 5s, 25s), 10s timeout per attempt. X-Switchboard-Signature header for verification.
  • webhook.GenerateSecret() — 32-byte hex random secret, auto-generated on task creation when webhook_url is set.
  • webhook.Payload struct: task_id, task_name, workflow_id, channel_id, status, completed_at, output, stage_data, error.

Task Webhooks

  • Task executor fires webhook on completion, failure, and budget breach. Runs in a goroutine — non-blocking.
  • webhook_secret column on tasks table (migration 027).
  • Secret auto-generated in task handler Create when webhook_url is provided.

Workflow Webhooks

  • webhook_url and webhook_secret columns on workflows table (migration 027).
  • Webhook delivered on workflow completion (both handler and tool paths).

task_create Tool

  • AI-invocable tool that spawns sub-tasks from workflow/team contexts.
  • Predicates: available in workflow channels and team channels only. Blocked in service channels (prevents recursive task spawning).
  • Scope inheritance: created tasks inherit parent channel's team scope.
  • Depth limit: service channel check prevents tasks creating tasks.
  • Parameters: name, prompt, schedule (cron or "once"), persona, model, max_tokens, system_prompt. Validates cron before creation.

Workflow on_complete Chaining Extraction

  • TriggerWorkflowOnComplete() extracted to tools/workflow.go as shared function. Used by both HTTP handler and workflow_advance tool.
  • Handles both on_complete chain config and webhook delivery.

Fixed

  • workflow_advance tool completion path now triggers on_complete chaining. Previously only the HTTP Advance endpoint fired chains — AI-driven stage advancement silently skipped them.

Changed

  • Workflow model gains WebhookURL, WebhookSecret fields.
  • WorkflowPatch gains WebhookURL field.
  • Task model gains WebhookSecret field.
  • workflow_instances.go triggerOnComplete method replaced with one-line delegation to shared tools.TriggerWorkflowOnComplete().

[0.27.2] — 2026-03-10

Summary

Task execution — the scheduler can now actually run completions. Headless executor wires tasks into the full completion pipeline (provider resolution, tool execution, budget enforcement, notifications). Admin panel gains a Tasks section for CRUD, run history, kill switch, and global task configuration.

Added

Core Tool Loop Extraction

  • CoreToolLoop + LoopSink interface. Single canonical implementation of multi-round LLM completion with tool execution. Replaces three duplicated implementations (SSE streaming, multi-model streaming, sync JSON). Budget enforcement built in: MaxTokens, MaxToolCalls, MaxRounds.
  • LoopResult.BudgetExceeded field tracks which limit was hit. Callers get budget status without inspecting loop internals.
  • Four sink implementations: sseSink (interactive SSE), sseModelSink (multi-model SSE), accumSink (sync JSON), HeadlessSink (scheduler).

Provider Resolution Extraction

  • ResolveProviderConfig() standalone function. Resolution chain: explicit config → channel config → owner's personal → global. Shared by completion handler and task executor.
  • BuildToolDefs() standalone function. Server-registered tools filtered by context predicates + browser extension tools + persona tool grant allowlist.
  • FilterToolDefsByGrants() for task-level tool grant narrowing.

Task Executor (scheduler/executor.go)

  • Headless completion pipeline. Resolves provider, builds messages + tools, runs CoreToolLoop with Streaming: false, persists assistant response in service channel, updates run record with tokens/tools/wall-clock, sends owner notification.
  • Budget enforcement. max_tokens, max_tool_calls from task definition. Wall clock via context.WithTimeout. Budget breach → budget_exceeded status + mandatory owner notification.
  • Provider routing. Task provider_config_id → channel → owner's personal BYOK → team → global. Full vault decryption path.
  • Tool grants. Persona grants + task-level grants applied as second-pass allowlist. No browser tools in headless mode.

Task Configuration (taskutil/)

  • LoadTaskConfig() reads from global_settings key "tasks". Fields: enabled, allow_personal, max_concurrent, default_max_tokens, default_max_tool_calls, default_max_wall_clock.
  • ApplyDefaults() fills zero-value budget fields on new tasks.
  • Full cron parsing via robfig/cron/v3. Replaces hand-rolled parseDailyCron. Supports 5-field cron + descriptors (@hourly, @daily).
  • ValidateCron() pre-create validation rejects invalid expressions.

Permissions + Global Config

  • task.create permission — gates task create/update/delete/run routes.
  • task.admin permission — for future admin-level task management.
  • Global config keys: tasks.enabled (kill switch), tasks.allow_personal, tasks.max_concurrent (scheduler poll limit).

Admin Panel

  • Tasks section under Workflows category. Two tabs: task list + configuration.
  • Task list: all tasks across users with status, schedule, run count, next run time. Action buttons: run now, view history, delete.
  • Run history: drill-down per task showing start/end times, tokens used, tool calls, wall clock, status badge, error messages.
  • Kill switch: cancel active run from run history view.
  • Configuration tab: toggle tasks enabled/personal, set max concurrent and default budget ceilings. Writes to global_settings key "tasks".

Routes

  • POST /api/v1/tasks/:id/kill — cancel active run (user + admin)
  • POST /api/v1/admin/tasks/:id/run — admin trigger
  • POST /api/v1/admin/tasks/:id/kill — admin kill
  • DELETE /api/v1/admin/tasks/:id — admin delete

Changed

  • stream_loop.go rewritten from 559 lines to 181 — thin wrappers over CoreToolLoop. Identical behavior, zero duplication.
  • completion.go syncCompletion rewritten as wrapper. resolveConfig and buildToolDefs delegate to standalone functions. -237 lines.
  • Scheduler startup moved from early init to after hub + notification service initialization. Executor needs both.
  • scheduler.New() signature: New(stores) → New(stores, executor). Executor is optional (nil = v0.27.1 behavior).
  • Cron computation in scheduler and task handler now uses robfig/cron/v3 via taskutil.NextRunFromSchedule. Create and update validate schedule before persisting.

Dependencies

  • Added github.com/robfig/cron/v3 v3.0.1

Changelog Additions — v0.26.0

[0.26.0] — 2026-03-10

Summary

Workflow engine — team-owned staged processes with AI intake, visitor-facing branded entry, human assignment queue, and admin builder UI. Channels become execution contexts that move through defined stages, driven by personas and monitored by team members.

Added

Workflow Definitions (v0.26.1)

  • Workflow CRUD. POST/GET/PATCH/DELETE /api/v1/workflows with name, slug (auto-generated from name if omitted), description, entry_mode (authenticated | public_link), is_active flag, branding JSONB, retention JSONB. UNIQUE(team_id, slug) with partial index for global workflows.
  • Stage CRUD. POST/GET/PUT/DELETE /api/v1/workflows/:id/stages/:sid. Stages have name, ordinal, persona_id, system_prompt, form_template (JSONB), history_mode (full | summary | fresh), assignment_team_id.
  • Stage reorder. PATCH /api/v1/workflows/:id/stages/reorder — transactional ordinal update from ordered_ids array.
  • Publish + versioning. POST /api/v1/workflows/:id/publish creates an immutable workflow_versions snapshot (stages + persona tool grants). GET /api/v1/workflows/:id/versions/:version retrieves any published version. Duplicate publish returns 409. Schema: workflows, workflow_stages, workflow_versions (migration 023).
  • Permission gating. Workflow create/update/delete requires workflow.create permission (existing permission constant).

Workflow Instances + Stage Transitions (v0.26.2)

  • Start instance. POST /api/v1/workflows/:id/start creates a workflow channel, pins the published version, binds stage 0 persona, sets allow_anonymous based on entry_mode. Returns channel_id + stage info.
  • Advance/reject/status. POST /channels/:id/workflow/advance (with optional form data merge), POST /channels/:id/workflow/reject (with required reason → system message), GET /channels/:id/workflow/status.
  • Workflow columns on channels. workflow_id, workflow_version, current_stage, stage_data (JSONB), workflow_status (enum: active/completed/stale/cancelled), last_activity_at (migration 024).
  • Staleness sweep. Background goroutine (1h tick) marks active workflow instances as stale when last_activity_at exceeds WORKFLOW_STALE_HOURS (default: 72, env-configurable, 0 to disable).
  • Stage data merge. Dialect-safe (Go-side JSON merge, no Postgres || operator). Data accumulated across stages in stage_data column.

Visitor Experience (v0.26.3)

  • Workflow landing page. GET /w/:id/:slug — public surface with branded layout (accent color, logo URL, tagline from workflow branding JSONB), persona name/icon, "Start" button, session resume link.
  • Visitor start flow. POST /api/v1/workflow-entry/:scope/:slug — creates channel + anonymous session, binds stage 0 persona, sets session cookie, returns redirect to /w/:channelId (existing chat surface).
  • DenyVisitor tool scoping. Workspace, git, memory tools already use DenyVisitor predicate (shipped in v0.26.0 foundation).

AI Intake + Assignment Queue (v0.26.4)

  • workflow_advance tool. AI-callable tool with RequireWorkflow predicate — only available inside workflow channels. Persona calls this when form data is collected. Triggers same transition as handler advance. Accepts data (JSON object) + summary (string for stage note title).
  • Form template injection. Completion handler injects current stage's form_template as a system prompt when channelType == "workflow". Tells the persona what fields to collect and when to call workflow_advance.
  • Stage notes. CreateWorkflowStageNote() persists collected form data as channel-scoped notes (title: "Stage N: {summary}", content: JSON).
  • Assignment queue. workflow_assignments table (migration 025): channel, stage, team, assigned_to, status (unassigned/claimed/completed), timestamps. Created automatically when advancing to a stage with assignment_team_id.
  • Assignment endpoints. GET /teams/:teamId/assignments (team queue), GET /workflow-assignments/mine (user's claimed), POST /workflow-assignments/:id/claim (optimistic lock), POST /workflow-assignments/:id/complete.

Workflow Builder UI (v0.26.5)

  • Admin workflows section. New "Workflows" category tab in admin surface. List view with name, slug, entry mode, active status, version. Detail view with editable fields, stage list, add/edit/delete stages, form template JSON editor, publish button, public URL display.
  • Queue sidebar section. "Queue" section in chat sidebar (after Channels). Shows user's claimed assignments with badge count. Click to navigate to workflow channel. Complete button per assignment.
  • Workflow API methods. workflow-api.js extends global API object with full workflow CRUD, stage management, publish, instance lifecycle, and assignment queue operations.
  • Workflow CSS. Stage row styling (draggable, ordinal badges), queue sidebar items, team queue panel, admin badges.

Foundation (v0.26.0)

  • Session cleanup job. Background goroutine deletes expired anonymous sessions with no messages. SESSION_EXPIRY_DAYS env var (default: 30).
  • ExecutionContext TeamID wiring. teamID propagated through completion, stream_loop, and messages handlers into ExecutionContext.
  • Stale code cleanup. Surface delete now removes static assets. Fixed stale pricing comment. Removed deprecated AllDefinitionsFiltered().
  • workflow.create permission. Already in permission constants.

Fixed

  • Gin route wildcard panic. /w/:id (chat) and /w/:scope/:slug (landing) used different param names at the same path depth — Gin panics on registration. Fixed: landing route uses /w/:id/:slug (same param name, disambiguated by depth). API visitor start moved to /api/v1/workflow-entry/:scope/:slug.
  • SQLite surface registry nil panic. SeedSurfaces(), IsSurfaceEnabled(), and EnabledSurfaceIDs() dereferenced nil Surfaces store on SQLite (no migration 022). Added nil guards with graceful fallbacks.

Migration Notes

  • DB wipe required. Migrations 023-025 add new tables/columns. Fresh chat_switchboard_dev DB recommended for dev deployments.
  • New env vars: SESSION_EXPIRY_DAYS (default 30), WORKFLOW_STALE_HOURS (default 72). Both optional.
  • Frontend files: 3 new JS files (workflow-api.js, workflow-admin.js, workflow-queue.js), 1 new CSS file (workflow.css). Templates updated to include them.

How to Create a Workflow

  1. Admin → Workflows tab → "+ New" → enter name
  2. Edit the workflow: set description, entry mode, toggle Active
  3. Add stages with names, persona assignments, form templates (JSON), history mode
  4. Publish to create an immutable version snapshot
  5. Share the public URL (/w/{scope}/{slug}) for visitor intake, or start instances via API (POST /workflows/:id/start)

Known Gaps (v0.27.0)

  • Team-scoped workflow management UI (team admins can only see workflows via admin panel, not the team settings surface)
  • Drag-and-drop stage reorder in builder UI
  • on_complete workflow chaining (column exists, nullable, not wired)
  • Workflow retention policies (column exists, not enforced)
  • Stage persona auto-switch in chat UI (backend binds persona, frontend doesn't reflect stage transition yet)

[0.25.4] — 2026-03-09

Added

  • Admin settings export/import. Export button downloads versioned JSON envelope (_type, _version, _exported_at, _switchboard_version, settings, policies). Import validates envelope, shows confirmation with key counts and metadata, writes each key via adminUpdateSetting(). Sensitive key warning on export.
  • Workspace rename/delete. Workspace picker rows show ✏ rename and 🗑 delete buttons. API.deleteWorkspace(id) method added (backend DELETE /workspaces/:id already existed). Confirmation dialog warns about permanent file deletion.
  • Bulk model visibility. Settings → Models page shows "X visible, Y hidden of Z total" with Show All / Hide All buttons.
  • Multi-provider live test failover. LIVE_PROVIDERS=venice,openrouter env var. tryCompletion() walks providers until one responds 200. setupAllProviders() tolerates individual setup failures. CI no longer fails on transient upstream 503s.

Fixed

  • Admin surface: 6 audit fixes. Stray > in back button, dual-bind race on back button, hardcoded People nav flash, closeAdmin() ignores return URL, openAdminSection() pushes history, Storage section empty (missing files.js script).
  • Banner overlapping surface content. Body changed to display:flex;flex-direction:column;height:100vh. Surface is flex:1;min-height:0. Banners get flex-shrink:0. Replaced fragile calc(100vh - ...) with proper flex distribution.
  • Scaling architecture. #surfaceInner wrapper in base.html — generic scale target via transform:scale(). No surface-specific selectors. Banners stay unscaled. Extension surfaces get scaling for free.
  • Group permissions 500. Postgres UpdateBuilder.SetNull() incremented argIdx without adding an arg — positional placeholders misaligned after first NULL column. SQLite version was already correct.
  • BYOK/Personas toggles not working. Admin saves to policies store (allow_user_byok), settings loader read from GlobalConfig (user_providers.enabled). Two different stores, two different keys. Fixed to read from Policies.GetAll().
  • Stale settings modal HTML. 225 lines of orphaned modal body content from pre-surfaces migration rendered directly in chat page flow (General tab with display:block always visible below chat input). Removed along with old admin panel modal (~320 lines total).
  • Team admin modal transparent. class="modal-content modal-lg"class="modal". Also fixed save-to-note modal in notes.js. Zero modal-content references remain project-wide.
  • Team admin modal layout. Duplicate display property (display:none;...;display:flex) made content always visible. Moved flex to CSS class .team-admin-content.
  • Team persona create button missing. Template lacked settingsTeamAddPersonaBtn button and settingsTeamAddPersona form container that JS expected.
  • Settings surface handler wiring. _initSettingsListeners() early-returned on missing settingsModel (only on General section), killing all handlers below including BYOK provider form, model visibility, team admin add-member/provider/persona. Restructured: settings features under data-surface guard, team admin features unconditional with ?..
  • Settings surface missing App.policies and App.models. On chat surface, app.js populates these. Settings surface called wrong API (getSettings() returns user prefs, not policies). Fixed to getPublicSettings() + fetchModels().
  • Model Roles section empty. Template had no userRolesConfig container for the roles section. loadUserRoles() early-returned on BYOK disabled or no personal providers with zero feedback. Added proper template section with notice text.
  • User settings NULL/null corruption. users.settings column: SQL NULL → Postgres COALESCE fix. JSON null literal → NULLIF(settings, 'null') + jsonb_typeof guard. Prevents null || {...} → [null, {...}] array concatenation. Both GetSettings and UpdateSettings fixed for Postgres and SQLite.
  • null.model_roles crash. Fresh users have NULL settings → API.getSettings() returned null → settings.model_roles threw. Added || {} fallback on all callsites.
  • Project files 403 for admins. ListByProject, UploadToProject, and loadAndAuthorize ownership checks had no admin bypass. Added c.GetString("role") != "admin" guard.

Migration Notes

  • No new migrations. All fixes are handler/template/JS changes.
  • Data fix required. Users with corrupted settings (JSON array or null literal) need manual cleanup — see release notes.

[0.25.3] — 2026-03-09

Fixed

  • Theme system mode broken. Theme.set('system') called removeAttribute('data-theme'):root CSS hardcoded dark → system always showed dark. Now resolves OS preference via matchMedia, sets data-theme to resolved value, registers listener for OS changes.
  • Resize bar jump on click. onStart now pins inline width/minWidth immediately after measuring via getBoundingClientRect(). Prevents snap-back during open-transition.
  • Debug modal no background/scroll. class="modal-content modal-lg"class="modal modal-wide". Modal body restructured as flex column with proper overflow.
  • Settings/admin back button. Stashes document.referrer in sessionStorage, nav links use location.replace(), back button reads stashed URL.
  • Profile save errors. Wrong element ID (settingsDisplayNameprofileDisplayName), wrong API routes (/users/me/profile, /auth/password/profile/password).
  • Profile data empty. getUserContext() only populated ID and Role from JWT. Now fetches full user record.
  • Scale slider range. max="120"max="175". Commit on mouse release, not live on input.

[0.25.2] — 2026-03-09

Added

  • Drag-resize utility. drag-resize.js — single primitive for workspace handle and pane split handles. Full-viewport overlay prevents iframes/CM6 from swallowing mousemove. Touch support. Transitions killed during drag.

[0.25.1] — 2026-03-09

Fixed

  • Minor surface integration fixes and CSS corrections from v0.25.0 deployment.

[0.25.0] — 2026-03-08

Added

  • Dynamic surfaces architecture. Go template engine (server/pages/) with base.html shell, surface-specific templates, per-surface script/CSS blocks. Surface manifest registry with SeedSurfaces() for DB persistence.
  • Surface registry. surface_registry table. Admin can enable/disable surfaces. Core surfaces: chat, admin, settings, editor, notes.
  • Component extraction. UserMenu, ModelSelector, FileTree, CodeEditor, NoteEditor extracted as standalone components with Go template partials + JS hydration.
  • Pane container. pane-container.js / pane-container.css — multi-pane layout with resizable splits. Used by editor surface.
  • Admin surface. Full-page admin at /admin/:section replacing the old modal. Category tabs (People, AI, Routing, System, Monitoring), left nav, section-specific scaffolding via admin-scaffold.js.
  • Settings surface. Full-page settings at /settings/:section replacing the old modal. Section-specific templates with server-rendered content areas.
  • Editor surface rebuild. File tree pane + code editor pane + chat assist pane. Surface manifest declares three panes with resizable splits.
  • base.html template shell. Banner → #surface → banner layout. Early theme flash prevention. Universal init block (Theme.init() + UI.restoreAppearance()). Common scripts: app-state.js, api.js, events.js, ui-primitives.js, ui-core.js.
  • Persona tool grants. renderPersonaForm() gains tool grants section — "All tools" checkbox with categorized tool checklist. loadToolList() fetches from /api/v1/tools. loadToolGrants() reads persona-specific grants.
  • window.__PAGE_DATA__ injection from surface data loaders. window.__USER__ from authenticated user context.

Changed

  • Surface navigation. UI.openSettings() and UI.openAdmin() navigate to full-page surfaces instead of opening modals.
  • CSS decomposition. Monolithic styles.css split into: variables.css, layout.css, primitives.css, modals.css, chat.css, panels.css, surfaces.css, splash.css, pane-container.css, chat-pane.css, user-menu.css, tool-grants.css, admin-surfaces.css.

Migration Notes

  • Migration 022: surface_registry table (UUID PK, unique surface_id, is_enabled, is_core, created_at).

[0.24.3] — 2026-03-07

Added

  • Anonymous session participants. Unauthenticated visitors can interact with workflow channels via ephemeral session identities. session_participants table stores channel-scoped session tokens with auto-generated display names ("Visitor #N"). Two entry paths: cookie-based (default, random token in sb_session cookie, 30-day expiry) and mTLS-based (cert fingerprint as stable session identity).
  • AuthOrSession middleware. Tries JWT auth first (Authorization header + sb_token cookie), falls back to session cookie for workflow channels. Validates channel is type=workflow with allow_anonymous=true before creating a session. Sets auth_type=session in context with session_id and channel_id.
  • Session-scoped API routes. POST /api/v1/w/:id/completions, POST /api/v1/w/:id/messages, GET /api/v1/w/:id/messages — all under AuthOrSession. Session participants can send messages and trigger completions within their bound channel only.
  • Workflow entry page. GET /w/:id renders a standalone chat UI via Go template (workflow.html). Minimal layout: channel title/description header, message list, input box, session identity footer. No sidebar, no settings, no navigation — purpose-built for anonymous intake.
  • SessionStore interface. Create, GetByToken, GetByID, ListForChannel, CountForChannel, Delete. Both Postgres and SQLite implementations.
  • SessionParticipant model. ID, SessionToken, ChannelID, DisplayName, Fingerprint, CreatedAt. Added as channel participant with participant_type=session, role=visitor.
  • channels.allow_anonymous flag. Boolean column (default false) gating session access. Middleware enforces: only type=workflow channels with allow_anonymous=true accept session auth.
  • Session-aware handlers. CreateMessage, ListMessages, and Complete all check isSessionAuth() and enforce channel scope via sessionCanAccessChannel(). Session participants bypass user-level budget and model allowlist checks (they use the workflow channel's allocated resources). Complete falls back to URL param for channel_id on workflow API routes.
  • Handler helpers. isSessionAuth(c) and sessionCanAccessChannel(c, channelID) in handlers/channels.go for consistent session identity checks.

Migration Notes

  • Migration 021: Creates session_participants table (UUID PK, unique session_token, FK to channels, display_name, fingerprint). Adds allow_anonymous boolean column to channels. Both Postgres and SQLite.
  • No new env vars. mTLS session path reuses existing AUTH_MODE and X-SSL-Client-Fingerprint header.

What Doesn't Ship

  • Workflow definitions and stage transitions (v0.25.0)
  • Session-to-user promotion ("create an account from your session")
  • Session expiry and cleanup (future housekeeping job)

[0.24.2] — 2026-03-07

Added

  • Fine-grained permission system. 12 permission constants using domain.action convention (model.use, model.select_any, kb.read, kb.write, kb.create, channel.create, channel.invite, persona.create, persona.manage, workflow.create, admin.view, token.unlimited). AllPermissions slice for UI rendering and validation.
  • Permission resolution. auth.ResolvePermissions() computes effective permissions as the union of the Everyone group plus all explicitly assigned group memberships. Admin users bypass checks entirely.
  • RequirePermission() middleware. Gin middleware with per-request permission caching — computed at most once regardless of how many permission gates are chained. GetResolvedPermissions() helper for conditional logic in handlers.
  • Permission-gated routes. POST /personas requires persona.create. PUT/DELETE /personas/:id requires persona.manage. POST /knowledge-bases requires kb.create. POST /knowledge-bases/:id/documents requires kb.write. POST /channels requires channel.create. POST /channels/:id/participants requires channel.invite.
  • Token budgets. Per-group daily and monthly token ceilings stored as BIGINT columns on groups. auth.ResolveTokenBudget() returns the most restrictive budget across all memberships. Pre-flight enforcement in the completion handler — queries usage_log for current period totals, returns 429 when exceeded. BYOK bypass: personal-scope providers skip budget checks entirely.
  • Model access control. Per-group allowed_models JSONB column (NULL = unrestricted). auth.ResolveModelAllowlist() unions all group allowlists — any group with NULL means unrestricted. Enforced in both the completion handler (403 on disallowed model) and the model list endpoint (filters response so users only see permitted models).
  • Everyone group. Implicit group with stable well-known ID (00000000-...0001), source=system, seeded in migration 020. All authenticated users receive its permissions without explicit membership. Editable in admin — replaces the previously planned DefaultUserPerms / global_config approach. source=system guard prevents deletion (returns 400 from the store layer).
  • Admin permissions endpoints. GET /api/v1/admin/permissions returns all valid permission strings. GET /api/v1/admin/users/:id/permissions returns effective resolved permissions for a user.
  • Admin group permissions UI. Group detail view expanded with three new sections: permission checklist (checkbox per permission with description), token budget fields (daily/monthly, empty = unlimited), and model allowlist multi-select (deduped by model_id, all-checked = unrestricted). Single save button sends all fields in one PUT.
  • API methods. adminListPermissions(), adminGetUserPermissions(userId).

Fixed

  • OIDC group creation compile error. auth/oidc.go line 412 passed string to *string field (Group.CreatedBy). Fixed to &createdBy.

Migration Notes

  • Migration 020: Adds permissions (JSONB, default '[]'), token_budget_daily (BIGINT), token_budget_monthly (BIGINT), allowed_models (JSONB) columns to groups table. Extends groups.source CHECK to include system. Drops NOT NULL on created_by (NULL for system-seeded groups). Seeds the Everyone group with model.use, kb.read, channel.create permissions.
  • Both Postgres and SQLite migrations included.
  • No new env vars.

[0.24.1] — 2026-03-07

Added

  • mTLS auth provider. server/auth/mtls.go — authenticates via reverse proxy headers (X-SSL-Client-DN, X-SSL-Client-Verify, X-SSL-Client-Fingerprint). Parses RFC 2253 distinguished names, auto-provisions users from cert CN, uses fingerprint as stable external identity. Configurable via MTLS_HEADER_DN, MTLS_HEADER_VERIFY, MTLS_DEFAULT_ROLE env vars.
  • OIDC auth provider. server/auth/oidc.go — OpenID Connect with authorization code flow. JWKS key caching with auto-refresh on rotation, RSA token validation, split-horizon issuer support (OIDC_ISSUER_URL for backend, OIDC_EXTERNAL_ISSUER_URL for browser). Claim extraction for preferred_username, email, name, groups, realm_access.roles. Role mapping via configurable OIDC_ADMIN_ROLE. Group sync: adds/removes OIDC-sourced group memberships on every login based on IdP groups claim.
  • OIDC login flow. GET /api/v1/auth/oidc/login redirects to IdP. GET /api/v1/auth/oidc/callback exchanges code for tokens, validates, auto-provisions user, issues internal JWT, redirects to login page with base64-encoded token fragment. Login page JS picks up #oidc_result=..., saves to localStorage, redirects to app.
  • Login page SSO button. Template adapts by AUTH_MODE: builtin shows username/password form, OIDC shows "Sign in with SSO" button, mTLS shows certificate prompt.
  • Keycloak integration test environment. docker-compose-keycloak.yml extends base compose with Keycloak 26 + pre-configured realm. ci/keycloak-realm.json includes switchboard client, group mapper, two test users (alice/user, bob/admin), two groups (engineering, leads). One command: docker compose -f docker-compose.yml -f docker-compose-keycloak.yml up.
  • QArgs() dialect adapter. Handles Postgres $N placeholder reuse for SQLite — expands args to match positional ? placeholders. Wrapper functions database.QueryRow(), database.Query(), database.Exec() (+ Context variants) replace database.DB.Query(database.Q(...)) pattern with automatic arg expansion.
  • QArgs unit tests. 5 tests covering reused placeholders, no-reuse passthrough, ILIKE rewrite, high placeholder numbers, Postgres no-op.
  • Channel list regression test. TestIntegration_ChannelListWithTypeFilter exercises types=direct,group query path that previously returned 500 on SQLite.

Fixed

  • SQLite _time_format DSN parameter. Removed _time_format=2006-01-02T15:04:05Z from modernc.org/sqlite connection string — this parameter is mattn/go-sqlite3-only and caused database ping: unknown _time_format on startup.
  • SQLite store wiring. main.go store initialization now uses sqliteStore.NewStores() when DB_DRIVER=sqlite instead of always using postgres.NewStores(). Fixes nil pointer panic in health accumulator goroutine.
  • nginx resolver for localhost. Changed set $backend http://localhost:8080 to http://127.0.0.1:8080 — IP literal doesn't require DNS resolution inside container.
  • nginx extension asset routing. Changed location /api/ to location ^~ /api/ — prevents location ~* \.(js)$ regex from intercepting /api/v1/extensions/.../main.js requests.
  • Channel list 500 on SQLite. Count query uses user_id = $1 OR ... participant_id = $1 — Postgres allows reusing $1 but SQLite's ? is positional. Fixed with QArgs arg expansion.
  • OIDC group sync FK constraint. findOrCreateOIDCGroup now receives the provisioning user's ID as created_by, satisfying the groups.created_by REFERENCES users(id) foreign key.

Migration Notes

  • Migration 019: oidc_auth_state table (ephemeral OIDC flow state), groups.source column (manual/oidc).
  • New env vars: 10 mTLS (MTLS_*) + 11 OIDC (OIDC_*) + AUTH_MODE extended to accept mtls/oidc.

[0.24.0] — 2026-03-06

Added

  • Auth provider abstraction. New server/auth/ package with Provider interface: Authenticate(), Register(), SupportsRegistration(), Mode(). Three modes defined: builtin (implemented), mtls (v0.24.1), oidc (v0.24.1). AUTH_MODE env var selects provider at startup. mTLS/OIDC fail-fast with clear error if selected before implementation lands.
  • Builtin provider. auth.BuiltinProvider extracts login/register logic from handlers/auth.go into the Provider interface. Identical behavior to v0.23.2 — reads {login, password} from request body, validates bcrypt hash, returns user with plaintext password as VaultHint for UEK unlock.
  • User handles. handle column on users table — unique, auto-generated from username via models.HandleFromName(). Used as the @mention identifier for human users. Editable. UniqueHandle() appends -2, -3 suffixes on collision (same pattern as persona handles).
  • Auth source tracking. auth_source column on users table (builtin/mtls/oidc). external_id column for IdP subject IDs or cert fingerprints. Composite unique index on (auth_source, external_id).
  • GetByHandle() and GetByExternalID() on UserStore interface — both Postgres and SQLite implementations.
  • Design document. docs/DESIGN-0.24.0.md — full spec for v0.24.0v0.24.3 (auth abstraction, mTLS/OIDC, permissions, anonymous sessions). All open questions resolved.

Changed

  • Auth handler refactored. AuthHandler now holds a provider auth.Provider field. Login() delegates to provider.Authenticate(), then handles vault unlock and JWT generation. Register() delegates to provider.Register(). generateTokens() response includes handle and auth_source.
  • BootstrapAdmin() / SeedUsers() backfill handle for existing users on startup (idempotent — skipped if handle already set). New users created with auth_source=builtin and auto-generated handle.
  • @mention resolution uses handles. resolveMention() steps 34 now query LOWER(handle) instead of LOWER(username). Backend and frontend aligned.
  • User search includes handles. GET /api/v1/users/search returns handle field and filters on handle alongside username and display_name.
  • Autocomplete matches on handle. channel-models.js filters user candidates by handle, username, and display name. The @mention token inserted into the input is the user's handle (not username).
  • User store queries updated. All SELECT statements in both Postgres and SQLite user stores include auth_source, external_id, handle. Unified scanOneUser helper in Postgres, scanOne in SQLite — eliminates per-method scan block duplication.
  • User list API. List() response includes auth_source and handle for all users.

Migration Notes

  • DB wipe required. Migration 018 adds three columns to users. Existing users backfilled with auth_source='builtin' and handle derived from username.
  • New columns: users.auth_source, users.external_id, users.handle.
  • New indexes: idx_users_handle (unique), idx_users_external_id (unique composite, partial).

[0.23.2] — 2026-03-06

Added

  • Unified active conversation. App.activeConversation = { id, type } replaces App.currentChatId and App.currentChannelId. All code paths (send, stream, model bar, session restore, sidebar highlight) keyed off App.activeId. setActive(id, type) method, activeType getter, getActiveChat() helper.
  • Group leader default response. When a type=group channel receives a message without an @mention, the leader persona responds. Leader resolved from persona_group_members WHERE is_leader via channel participants.
  • @all fan-out. @all in message content routes to every persona participant. Depth-1 only — responses from @all do not trigger further chains.
  • Human message attribution. Messages from other human participants show their avatar and display name instead of "You". Persona attribution verified for loaded history in channel context.
  • Channel lifecycle. Archive action via context menu (sets is_archived, archived_at). Delete gated by channel_retention.mode policy (flexible allows delete, retain forces archive-only). Admin purge with purge_after_days floor. Retention config keys in global_config.
  • Participant mutation guards. DMs block removal below 2 human participants. Groups block removal of last persona (returns 400 with clear error).
  • Channel context banner. Slim bar below chat header showing ai_mode, topic, and routing hints. Adapts by conversation type (DM: partner name + @mention hint; Group: leader/all routing; Channel: ai_mode + topic).
  • NotifyUserMention via WebSocket. Replaced broken gin context interface cast with direct hub.SendToUser(). Delivers user.mentioned event with channel_id, from_user, content preview.
  • User mention notification toast. user.mentioned WebSocket event handler shows toast with content preview and increments unread badge.

Fixed

  • Channel persistence through refresh. loadChannels() read resp.channels but ListChannels returns paginated response with data key. Channels vanished on reload.
  • Folder drag-and-drop. Three compounding issues: folderId never mapped in loadChats(), folder groups had no drag handlers, no unfiled drop zone. All fixed.
  • DM creation 404. Route didn't exist. Added GET /api/v1/users/search?q= endpoint. Rewrote DM creation UI from text input to lazy search modal.
  • Channel ⋯ menu inconsistency. Replaced ✕ delete button with ⋯ hover button matching folder pattern.
  • Folder ⋯ button reflow. display:none/block caused arrow shift on hover. Changed to visibility:hidden/visible.
  • Folder delete spill. Now shows three-button modal: "Keep chats" / "Delete chats too" / Cancel.
  • Unread subquery dependency. Query used last_read_message_id (migration 017) which may not exist. Rewrote to use last_read_at (migration 005).
  • Settings Models section. Template section fell through to generic div. Added dedicated template section.
  • u.avataru.avatar_url in ListMessages JOIN and treepath/path.go resolveSenderInfo().

Migration Notes

  • Migration 017: last_read_message_id column on channel_participants.

[0.23.1] — 2026-03-05

Added

  • Conversation type taxonomy. Five types: direct (1:1 AI chat), dm (human-to-human), group (multi-participant), channel (named persistent space), workflow (staged process, deferred). Channel type constraint extended.
  • ai_mode on channels. auto (AI responds to every message), mention_only (AI silent unless @mentioned), off (AI disabled). Completion handler checks ai_mode before dispatching.
  • Three-section sidebar. Projects → Channels → Chats. Each section independently collapsible with localStorage persistence. Channels sorted by last activity. Chats keep existing time-grouped behavior.
  • Folder system. chat_folders table with full CRUD. Drag chats into/out of folders. Folder context menu: rename, delete. folder_id column on channels.
  • DM plumbing. resolveMention() extended to resolve users (exact + prefix on username, self-mention blocked). User @mention skips AI, delivers notification. DM creation flow with user search.
  • Presence heartbeat. user_presence table. POST /presence/heartbeat (30s upsert), GET /presence?users=... (90s threshold). Client heartbeat interval with visibility pause/resume. Presence WebSocket events.
  • Channel participant CRUD. channel_participants handler: list, add, update role, remove. Auto-created on channel creation. DM participants auto-added from req.Participants.
  • Persona groups wired. CRUD endpoints for persona_groups and persona_group_members. is_leader flag on members.
  • topic column on channels for header display.
  • User search endpoint. GET /api/v1/users/search?q= — returns id, username, display_name for active users (max 20, excludes caller).

Changed

  • CreateChannel accepts type field (default direct). ListChannels supports ?types=dm,channel multi-filter.
  • Channel sidebar rendering. renderChannelsSection() shows # for channels, person icon for DMs. Online dots from presence data. Unread badges.
  • Chat list filtering. renderChatList excludes type=channel and type=dm — only direct/group chats appear in the Chats section.

Migration Notes

  • Migration 016: ai_mode, topic on channels. user_presence table. Channel type constraint extended to include dm, channel.

[0.23.0] — 2026-03-05

Added

  • @mention routing. Type @persona-handle or @model-id in any chat to route the completion to a specific persona or model. Works in all chats — no roster or group setup required. Resolution order: persona handle (exact, then prefix) → model catalog (exact, then prefix). Backend resolveMention() does direct DB lookup against personas.handle and model_catalog.model_id.
  • Persona handles. Every persona gets a unique handle field (e.g. veronica-sharpe) auto-generated from the name. Editable in the persona form. Stored in personas.handle with a unique index. Used as the @mention identifier — no spaces, no ambiguity.
  • @mention autocomplete. Typing @ in the chat input shows a filterable popup of all enabled models and personas with avatars, display names, @handle hints, and provider info. Works in any chat. Matches against handle, model ID, and display name. Arrow keys + Enter/Tab to select, Escape to dismiss.
  • @mention pill rendering. @mentions in message content render as styled accent-colored pills. Handles and display names both highlighted. Skipped inside <code> and <pre> blocks.
  • AI-to-AI chaining. When a persona response contains an @mention of another persona, a follow-up completion fires automatically. Delivered via WebSocket (message.created event). Chain depth capped at 5. Self-mention blocked. Uses the same resolveMention() as user @mentions.
  • Participant list injection. System prompt includes a list of available @mentionable personas so LLMs know who they can invoke. Injected in loadConversation() after memory hints. Excludes the current persona (no self-mention). Empty when no other personas exist (zero overhead on 1:1 chats).
  • Context boundaries. When switching personas via @mention, a system message is injected before the user's message telling the target persona to ignore previous personas' styles. When a non-persona model responds after persona messages exist in history, a boundary tells it to use its own natural style.
  • Provider proxy support. provider_configs table gains proxy_mode (system/direct/custom) and proxy_url columns. Provider HTTP clients use proxy_mode to configure transport: system = env-based (HTTP_PROXY), direct = no proxy, custom = explicit URL. All four providers (Anthropic, OpenAI, OpenRouter, Venice) updated.
  • Persona groups schema. persona_groups and persona_group_members tables added (migration 004). Structural foundation for saved roster templates — CRUD and FE not yet implemented.
  • Per-provider model preferences. user_model_settings unique key widened to (user_id, model_id, provider_config_id). Same model from different providers gets independent visibility toggles. Frontend uses composite keys throughout.
  • Channel participants. channel_participants table with polymorphic participant_type (user/persona/session), role (owner/member/observer). CRUD endpoints: list, add, update role, remove. Auto-created on channel creation.
  • Persona avatar resolution. Assistant messages resolve persona portraits through channel roster → participant data → App.models lookup. Streaming messages also resolve avatars.
  • Save message to note. Per-message "Note" button (visible on hover) opens a modal to create a new note or append to an existing note from any message. Supports text selection — selected text is saved instead of full message.
  • Group chat creation. "Group Chat" option in New Chat dropdown. Persona picker with handles, scope badges, and model info. Creates channel + adds persona participants.
  • Chat type indicators. Sidebar shows 👥 for group chats, ⚙ for workflow channels.

Changed

  • Channel models constraint. channel_models unique constraint split into two partial indexes: raw models keyed on (channel_id, model_id, provider_config_id) WHERE persona_id IS NULL, persona entries keyed on (channel_id, persona_id) WHERE persona_id IS NOT NULL. Two personas on the same underlying model now get separate roster entries.
  • Channel models queries. GetModels and GetModelByID in both Postgres and SQLite stores now JOIN personas table to carry handle data alongside display name and persona ID.
  • User message alignment. User bubbles use flex: initial with max-width: 85% to shrink-wrap content and right-align within the centered 768px column.
  • Channel list loading. loadChats() no longer filters by type=direct — all channel types (direct, group, workflow) load on refresh.
  • Autocomplete CSS. Replaced legacy CSS variable names (--bg-primary, --text-secondary, etc.) with current theme variables (--bg-surface, --bg-raised, --text-3, etc.) across all channel-models.css.
  • Persona form. Added @mention handle field with auto-generation from name, monospace styling, and edit-locks (manual edit stops auto-gen). Handle included in getValues(), setValues(), and clearForm().
  • Model dropdown. Persona entries show @handle hint next to display name in accent color monospace.
  • Completion chain. Rewritten to use resolveMention() directly instead of roster-based mention parsing. No roster, participant list, or channel_models dependency. Same function for user→LLM and LLM→LLM routing.

Fixed

  • Notes modal visibility. _showSaveToNoteModal now creates overlay with modal-overlay active class (was missing active, modal was invisible).
  • Single @mention routing. Single-target @mentions no longer reload conversation (which caused duplicate user messages that broke Anthropic's API). System prompt swapped in-place on existing messages array.

Migration Notes

  • DB wipe required. Migrations 003, 004, and 005 modified. No upgrade path from previous schema — drop and recreate dev DB.
  • New columns: personas.handle, provider_configs.proxy_mode, provider_configs.proxy_url.
  • New tables: persona_groups, persona_group_members, channel_participants.
  • New indexes: idx_personas_handle (unique), idx_channel_models_raw (partial), idx_channel_models_persona (partial).

[0.22.8] — 2026-03-04

Changed

  • Naming cleanup: preset → persona. Unified domain terminology across the entire codebase. Routes /presets/personas (user, team, admin). JSON response keys, request fields, Go struct fields, handler names, JS functions, CSS classes, DOM IDs, template strings, display text — all consistently use "persona". File renamed: presets.gopersonas.go. Dual JSON keys ("personas" + "presets") collapsed to single "personas" key. Config key user_presetsuser_personas. Banner color presets intentionally left as "presets" (different domain concept).
  • Naming cleanup: APIConfigID → ProviderConfigID. All Go structs and JSON fields now use provider_config_id consistently.
  • Removed aliases. chat_id field removed from completion handler. /models route alias removed (use /models/enabled).
  • Shared app-state.js. Extracted App state object, fetchModels(), and resolveCapabilities() from chat-only app.js into new app-state.js loaded by base.html for all surfaces. Settings and admin surfaces now have access to App state for model dropdowns, settings, and user context.
  • Auth tokens loaded for all surfaces. API.loadTokens() now runs in base.html inline script so settings, admin, editor, and notes surfaces can make authenticated API calls.
  • Script loading consolidated in base.html. ui-core.js moved from per-surface loads to base.html. ui-primitives-additions.js (never loaded by any surface) now loaded in base.html. All surfaces get the complete shared stack: app-state → api → events → ui-primitives → ui-primitives-additions → ui-core → pages.

Added

  • Settings surface functions. UI.loadGeneralSettings() populates the general settings form (model dropdown, system prompt, temperature, max tokens, thinking toggle) with server data. UI.saveAppearance() persists theme, scale, and font size. UI.loadTeamsSettings() renders team membership on the settings teams tab.
  • Design documentation. docs/DESIGN-SURFACES.md (four-layer surface architecture, extension hooks, trust model), docs/ICD-API.md (domain-organized API contract, 240 routes), docs/ICD-AUDIT.md (cross-reference audit with resolution status).

[0.22.7] — 2026-03-03

Added

  • Theme system. New theme.css provides complete light/dark theming via CSS custom properties with [data-theme] attribute switching. Variables cover backgrounds, surfaces, text, accents, borders, shadows, and component-specific tokens. System preference auto-detection via prefers-color-scheme media query. Old variables (--bg-primary, --text-primary) bridged via fallback chains (var(--bg, var(--bg-primary, #0e0e10))) — zero breakage of existing CSS.
  • ChatPane component. New chat-pane.js provides a mountable, self-contained chat pane factory replacing the hardcoded singleton pattern. ChatPane.create(opts) returns an instance with renderMessages(), appendChunk(), finalizeStream(), appendTyping(), removeTyping(), scrollToBottom(), showWelcome(), clear(), getInputValue(), setInputValue(), focusInput(), destroy(). Lookup via ChatPane.get(id), ChatPane.forChannel(channelId), ChatPane.active(). Each instance owns its own DOM elements and channel binding.
  • Chat pane template component. New components/chat-pane.html Go template partial: {{template "chat-pane" dict "ID" "main"}} renders a complete chat scaffold with messages container, input bar, model selector, send button, and toolbar mount points. Used by editor surface assist pane.
  • Splash/login surface. Login page rewritten from minimal form to split-panel splash layout: animated grid canvas on hero side, tabbed auth card (Login + Register) on right. Registration form includes avatar upload, display name, email. Powered by new pages-splash.js module with Pages.initSplash(), grid animation, and POST /api/v1/auth/register + PUT /api/v1/users/me/avatar integration.
  • UI primitives additions. New ui-primitives-additions.js extends the shared primitive library: Toast (show/success/error/warning/info with auto-dismiss stacking), renderBadge() (6 color variants), renderIcon() (20+ SVG icon set), renderIconBtn() (icon button factory with active state), Theme (init/set/get/resolved/renderToggle with localStorage persistence), showConfirmDialog() (enhanced with variant/callbacks), mountAvatarUpload() (file input with preview).
  • Settings BYOK gate. Settings surface nav now conditionally shows "My Providers", "Model Roles", and "Usage" tabs only when BYOK is enabled. Server-side gate via {{if .Data.BYOKEnabled}} reads GlobalConfig key user_providers.enabled. Includes BYOK status indicator in nav footer with UEK encryption notice.
  • Settings User Personas gate. Settings surface conditionally shows "My Personas" tab when user-created personas are enabled. Server-side gate via {{if .Data.UserPersonasEnabled}} reads GlobalConfig key user_presets.enabled.
  • Settings Models tab. New "Models" nav link in settings surface for user model visibility preferences.
  • Editor assist pane. Editor surface now includes a chat-pane template component for the right-side assist pane with split handle, wired up via ChatPane.create() on DOM ready.

Changed

  • base.html template. Added data-theme attribute on <html> element for theme system. Google Fonts preconnect + DM Sans / JetBrains Mono stylesheet. theme.css loaded before styles.css. New shared script tags for ui-primitives-additions.js and chat-pane.js (available on all surfaces). Added window.__SETTINGS__ global from PageData.SurfaceSettings.
  • PageData struct (pages.go). New fields: Theme (string), SurfaceSettings (any, serialized to __SETTINGS__), InstanceName, LogoURL, Tagline, RegistrationOpen. Render() defaults Theme to "dark" when unset.
  • RenderLogin() (pages.go). Now calls loadBranding() and isRegistrationOpen() to populate splash/login template fields from GlobalConfig keys branding.* and registration.enabled.
  • SettingsPageData (loaders.go). Added BYOKEnabled and UserPersonasEnabled boolean fields. settingsLoader() reads from GlobalConfig keys user_providers.enabled and user_presets.enabled.
  • settings.html template. Reorganized nav: base tabs (General, Appearance, Models, My Presets) always visible; BYOK section (My Providers, Model Roles, Usage) gated; User Personas gated; then Knowledge, Memory, Notifications, Teams. Added my-personas to dynamic section loaders.
  • editor.html template. Added chat-pane component in right-side assist pane with split handle and ChatPane.create() initialization.

New Files

  • src/css/theme.css — 305 lines: CSS custom properties, shared components, surface layouts
  • src/js/chat-pane.js — 162 lines: mountable chat pane factory
  • src/js/ui-primitives-additions.js — 199 lines: Toast, Badge, Icons, Theme, confirm, avatar
  • src/js/pages-splash.js — 154 lines: splash init, login, register, grid canvas
  • server/pages/templates/components/chat-pane.html — 23 lines: reusable template partial

[0.22.6] — 2026-03-03

Fixed

  • Split FE/BE deployment: page route proxying. Frontend container now supports BACKEND_URL env var. When set, the entrypoint generates nginx proxy_pass blocks for page routes (/, /login, /chat/:id, /admin/*, /editor/*, /notes/*, /settings/*) so Go template rendering works in k8s split-image deployments. When unset, behavior is unchanged (SPA fallback). Both BASE_PATH="" and BASE_PATH="/prefix" variants supported.
  • CI test timeout. All go test commands now include -timeout 8m to prevent indefinite hangs from runner resource contention or race-detector compilation stalls. Previously used Go's 10-minute default with no goroutine dump on timeout.

Removed

  • router.js (322 lines). Client-side hash router fully replaced by server-side page routes. All Router.* call sites in chat.js replaced with history.replaceState() for URL updates. All typeof Router guards in app.js removed.
  • surfaces.js (368 lines). Client-side surface registry replaced by Go template surface architecture. Extension API stubs (surfaces.register, surfaces.activate, etc.) now log deprecation warnings. surfaces.getCurrent() returns window.__SURFACE__ from server-rendered page data.
  • index.html SPA shell (1,296 lines → 16 lines). Original monolithic SPA entry point replaced with a minimal redirect page. All real routes now served by Go templates; index.html only serves as nginx try_files fallback for unknown paths.
  • editor-mode.js old Surfaces paths (~260 lines). Removed _register(), openDirect(), _unregister(), _activate(), _deactivate(), _embedChat(), _returnChat(), and full check()/checkStartup() implementations. Only mountServerRendered() remains as the entry point. check() and checkStartup() kept as no-ops for backward compat with projects-ui.js callers.
  • SPA-only entrypoint code path. docker-entrypoint-fe.sh now requires BACKEND_URL (fails fast if unset). Removed dead %%BASE_HREF%%, %%ENVIRONMENT%%, %%BRANDING_JSON%% injection into index.html. Removed branding config file loading (branding now handled by Go templates).

Changed

  • docker-entrypoint-fe.sh: Simplified from 227 → 184 lines. Requires BACKEND_URL. Removed SPA-only fallback path.
  • nginx.conf (unified container): Consolidated repeated proxy_set_header blocks. Uses $backend variable. Page routes proxy to Go backend for template rendering.
  • base.html: Added __ENV__ and __BRANDING__ globals for JS compatibility (previously injected by index.html sed).
  • server/pages/pages.go: Added Environment field to PageData, populated from config.
  • extensions.js: ui.replace(), ui.restore(), and surfaces.* API methods now log deprecation warnings instead of calling removed Surfaces system.
  • app.js: Removed Surfaces.init(), EditorMode.checkStartup(), and Router.init() calls. Simplified to direct sessionStorage chat restore.
  • chat.js: Replaced Router.update() calls with history.replaceState() for URL-bar sync on chat selection/creation.
  • repl.js: Removed surfaces.js import reference.

[0.22.5] — 2026-03-03

Added

  • Go template engine. Server-side HTML rendering via html/template with //go:embed for compiled-in templates. Custom FuncMap with helpers (roleFilterType, toJSON, hasPrefix, dict). CSP nonce generation per request. Replaces monolithic index.html + client-side DOM construction with composable, server-rendered surfaces.
  • Surface architecture. Each page route renders a dedicated surface template that owns its full layout below the classification banner. Surfaces compose from reusable components (model-select, team-select, file-upload) without sharing CSS layout rules. Banner height handled via CSS custom properties (--banner-top-h, --banner-bot-h, --surface-h).
  • Chat surface. Server-rendered shell at / and /chat/:chatID. Go template renders banner + layout + script tags; existing JS builds DOM inside the container. Bridge pattern preserves all current chat functionality.
  • Admin surface. Full admin panel at /admin/:section with 5 category tabs (People, AI, Routing, System, Monitoring) and section sidebar. Server-rendered pages for providers, models, teams, users, and settings. Hybrid fallback for JS-loaded sections (groups, presets, knowledge, memory, health, capabilities, extensions, storage, usage, audit, stats).
  • Editor surface. Server-rendered layout shell at /editor/:wsId with proper CSS height ownership. mountServerRendered() method on EditorMode bypasses the Surfaces registry for Go template mode. Dedicated CSS owns full viewport below banners — no layout collision with other surfaces. (Fixes bugs #4, #5: editor layout collapse)
  • Notes surface. Server-rendered layout at /notes/:noteId with notes-main + notes-assist split. Boot script calls openNotes() in standalone mode. Responsive: assist pane hidden on mobile.
  • Settings surface. Full-page settings at /settings/:section replacing modal-based settings. Left nav with all sections (general, appearance, providers, models, presets, roles, knowledge, memory, notifications, usage). General and Appearance sections server-rendered with form controls; dynamic sections populated by existing JS.
  • Login page. Standalone Go template at /login with cookie-based auth. Sets redirect_after_login cookie for post-login navigation.
  • AuthOrRedirect middleware. Cookie-based JWT validation for page routes. Reads sb_token cookie, redirects to /login on missing/invalid token (instead of 401 JSON). RequireAdminPage() helper aborts with 403 for non-admin users. Skips auth entirely in unmanaged mode.
  • Cookie-based auth sync. api.js saveTokens() now sets sb_token cookie alongside localStorage on every token save/refresh. clearTokens() clears the cookie. Bridges API auth (Bearer header) with page auth (cookie).
  • Reusable model-select component. Go template partial renders <select> with models filtered by type. Server passes filtered list; client-side cascade handler re-filters from window.__PAGE_DATA__.models on provider change. (Fixes bug #1: admin model roles missing embedding models)
  • Reusable team-select component. Go template partial renders team dropdown with pre-populated options from server data loader. (Fixes bug #2: routing policy team scope as text field)
  • Reusable file-upload component. Go template partial for drop zone + file picker. Used by editor surface.
  • Editor file upload. Upload button in editor toolbar + drag-and-drop on file tree with visual feedback. API.uploadWorkspaceFile() sends File objects as raw body to existing WriteFile handler (binary-safe, respects workspace quota). Auto-opens single uploaded text files. (Fixes bug #3: unable to upload files to a project)
  • Page route data loaders. Each surface registers a loader that pre-fetches exactly what its template needs: chatLoader, adminLoader (with section-specific data), editorLoader, notesLoader, settingsLoader. No over-fetching, no client-side fetch waterfall.
  • Admin provider CRUD. Server-rendered provider table with add/edit form, type dropdown, sync button. Client-side handlers for create, update, delete, sync operations.
  • Admin model table. Server-rendered catalog table with client-side search + type/provider filters.
  • Admin user management. Server-rendered user table with search, role edit, enable/disable toggles.
  • Admin settings page. Registration policy, permissions, banner config, and system prompt — all server-rendered with save handlers.
  • pages.js client handlers. Role CRUD, routing policy CRUD, provider/model/team/user management, settings save — all working against existing API endpoints with proper token resolution.

Changed

  • nginx.conf: Added page route proxy blocks (/, /login, /chat, /admin, /editor, /notes, /settings) before SPA fallback. Static assets still served directly by nginx. Removed /legacy proxy block.
  • server/main.go: Template engine init, pages.SetVersion(), route wiring for all surfaces with auth middleware groups. Removed /legacy fallback route.
  • src/js/api.js: saveTokens() / clearTokens() sync sb_token cookie. New uploadWorkspaceFile() method for binary file upload to workspaces.
  • src/js/editor-mode.js: Upload button + hidden file input in header. Drag-and-drop on file tree container with .drag-over visual state. _uploadFiles() method with text-file auto-open. mountServerRendered() for Go template boot path.
  • src/css/editor-mode.css: .drag-over style for file tree drop target.

Fixed

  • Bug #1: Admin model roles now show embedding models. Server-side roleFilterType() sets FilterType="embedding" for the embedding role; model-select component filters by model_type.
  • Bug #2: Routing policy team scope uses proper dropdown instead of free-text field. adminLoader pre-fetches team list; team-select component renders options.
  • Bug #3: Editor file upload works. Upload button + drag-and-drop + uploadWorkspaceFile() API method.
  • Bug #4: Editor chat assist pane is functional. Server-rendered layout with dedicated mount points.
  • Bug #5: Editor and chat pane no longer squashed. Each surface owns its full viewport height via dedicated CSS — no shared flex container conflicts.

Removed

  • /legacy route and nginx proxy block. All page routes are now server-rendered.

[0.22.4] — 2026-03-02

Added

  • Rate limit tracking. Provider health windows now track HTTP 429 / rate limit events separately from general errors. New rate_limit_count column on provider_health table. RecordRateLimit() method on health accumulator. Completion handler and stream loop classify HTTP 429 / rate limit / Too Many Requests errors automatically.
  • Auto-disable policy. Providers that report "down" status for N consecutive hourly windows are automatically deactivated. Configurable via PROVIDER_AUTO_DISABLE_THRESHOLD env var (default: 3, set to 0 to disable). AutoDisabler interface on health store, checkAutoDisable() runs on each flush cycle.
  • Tool health tracking. Built-in tools (web_search, url_fetch, etc.) now record success/error/latency to tool_health table via the health accumulator. RecordToolSuccess() / RecordToolError() methods. Both tool execution sites in stream loop instrumented. ToolHealthWindow and ToolHealthSummary models.
  • Search provider health tracking. web_search and url_fetch tool calls are now included in the tool health pipeline with per-invocation latency and error recording.
  • Project-specific file uploads. POST /api/v1/projects/:id/files (multipart upload) and GET /api/v1/projects/:id/files (list). project_id column on attachments table. Files are stored under projects/{id}/ prefix. Project access verified via ownership or team membership. Files queued for text extraction if enabled.
  • Workspace settings UI. Git configuration section in project settings panel: remote URL, branch, credential selector. Appears when a workspace is bound. Saves via PATCH /api/v1/workspaces/:id. updateWorkspace() and listGitCredentials() API client methods.
  • PDF/DOCX export via pandoc. New POST /api/v1/export endpoint converts markdown content to PDF or DOCX using pandoc. Editor export dropdown now includes "Export as PDF" and "Export as DOCX" options. Graceful error handling when pandoc is unavailable.
  • Project files UI. File list and upload button in project settings panel. Shows file names and sizes. Multi-file upload support.
  • API client methods. projectUploadFile, projectListFiles, exportDocument, updateWorkspace, listGitCredentials.

Changed

  • health/accumulator.go: Added rateLimitCount to bucket, RecordRateLimit(), RecordToolSuccess(), RecordToolError(), flushTools(), checkAutoDisable(), SetAutoDisable(), AutoDisabler interface, toolBucket struct. Store interface extended with UpsertToolWindow(), ListAllToolCurrent().
  • handlers/completion.go: HealthRecorder interface extended with RecordRateLimit(), RecordToolSuccess(), RecordToolError(). recordHealth() classifies HTTP 429 errors.
  • handlers/stream_loop.go: Both tool execution sites now record tool health. recordHealthFn() classifies rate limit errors.
  • models/models.go: RateLimitCount field on ProviderHealthWindow. ToolHealthWindow, ToolHealthSummary types. ErrorCount, RateLimitCount, TimeoutCount fields on ProviderHealthSummary. ProjectID field on Attachment.
  • store/postgres/health.go: All queries updated for rate_limit_count. UpsertToolWindow(), ListAllToolCurrent(), DeactivateProvider() methods.
  • store/sqlite/health.go: Full rewrite with rate_limit_count, tool health, and auto-disable support.
  • store/postgres/attachment.go: project_id in cols, scan, create, and new GetByProject().
  • store/sqlite/attachment.go: Same project_id additions.
  • store/interfaces.go: GetByProject() on AttachmentStore.
  • config/config.go: ProviderAutoDisableThreshold field + env var loading.
  • main.go: SQLite health store branching, auto-disable wiring, export handler route, project file routes.
  • handlers/health_admin.go: Health summary includes error_count, rate_limit_count, timeout_count.
  • src/js/ui-admin.js: Health dashboard cards show "Rate Limits" count with warning color.
  • src/js/projects-ui.js: Git settings section, project files section, file upload handler.
  • src/js/editor-mode.js: PDF and DOCX export menu items and handler.
  • src/js/api.js: 5 new API client methods.

Database

  • Migration 015 (Postgres) / 014 (SQLite): rate_limit_count column on provider_health, tool_health table, project_id column on attachments.

[0.22.3] — 2026-03-02

Added

  • Provider admin UI: "Routing" category. New admin category with three sections: Health, Routing, and Capabilities. Accessible from admin panel sidebar navigation.
  • Health dashboard. Admin section showing per-provider status badges, request counts, error rates, average/max latency, timeout counts, and last error messages. Refresh button for live updates. CSS grid layout with responsive cards.
  • Routing policy builder. Full CRUD UI for routing policies: create/edit form with name, priority, type (provider_prefer/team_route/cost_limit/model_alias), scope (global/team), team ID, JSON config editor, active toggle. List view with edit/enable/disable/delete actions.
  • Routing dry-run test panel. Input model + user ID, evaluates against all active policies with live health status, displays ranked candidates with status badges and selection reasoning.
  • Capability override viewer. Table listing all model capability overrides with model ID, provider config, field, value, and delete button. Explains auto-detection fallback when no overrides exist.
  • provider_status on models/enabled response. GET /api/v1/models/enabled now includes provider_status field ("healthy"/"degraded"/"down"/"unknown") per model from live health data.
  • ModelHandler with health enrichment. Extracted model listing into dedicated handlers/capabilities.go with SetHealthStore() for health status injection. Builds health map from current hourly windows.
  • GET /api/v1/admin/capability-overrides endpoint. Returns all capability overrides across all models (admin view).
  • API client methods. adminGetAllProviderHealth, adminListRoutingPolicies, adminGetRoutingPolicy, adminCreateRoutingPolicy, adminUpdateRoutingPolicy, adminDeleteRoutingPolicy, adminTestRouting, adminListCapabilityOverrides, adminDeleteModelCapability.

Changed

  • src/js/ui-admin.js: New "routing" admin category with health/routing/capabilities sections. ADMIN_SECTIONS, ADMIN_LABELS, ADMIN_LOADERS updated. Added loadAdminHealth(), loadAdminRouting(), loadAdminCapabilities(), _showRoutingForm(), _toggleRoutingPolicy(), _deleteRoutingPolicy(), _runRoutingTest(), _deleteCapOverride().
  • src/js/api.js: 9 new API client methods for health, routing, and capability admin endpoints.
  • src/index.html: New admin category button for "Routing". Three new admin-section-content divs: adminHealthTab, adminRoutingTab, adminCapabilitiesTab.
  • src/css/styles.css: .admin-health-grid and .admin-health-card styles.
  • models/models.go: ProviderStatus field on UserModel struct.
  • handlers/capabilities.go: Extracted ModelHandler from inline handlers. ListEnabledModels enriches response with provider health status. ResolveModelCaps canonical capability resolver.
  • main.go: ModelHandler created with SetHealthStore() wiring. Capability override admin routes registered.

[0.22.2] — 2026-03-02

Added

  • Routing policy engine. Rules-based request routing evaluated between "what model was requested" and "which provider serves it." routing/ package with evaluator, fallback runner, and type-safe policy model.
    • provider_prefer: Ordered list of preferred provider configs — fallback to others if preferred are unavailable.
    • team_route: Restrict a team to only use specific provider configs (team-scoped access control).
    • cost_limit: Filter out providers above a heuristic cost threshold per request.
    • model_alias: Map alias names ("fast", "smart") to specific provider+model pairs.
  • Health-aware routing. Candidates sorted by health status (healthy > degraded > unknown > down). skip_down flag removes down providers from consideration when healthy alternatives exist. All-down graceful degradation: keeps candidates rather than returning empty.
  • Fallback runner. routing.RunWithFallback() tries candidates in order with configurable retry depth. Logs each failure and tries the next candidate. Returns the winning candidate and updated decision with fallback depth.
  • routing_policies table. New migration (Postgres 014, SQLite 013) with scope (global/team), priority ordering, JSONB config, and active flag. Index on (is_active, priority) for efficient policy loading.
  • Routing policy admin CRUD. Full REST API for policy management:
    • GET /api/v1/admin/routing/policies — list all policies
    • GET /api/v1/admin/routing/policies/:id — get single policy
    • POST /api/v1/admin/routing/policies — create policy (validates type, scope, team_id)
    • PUT /api/v1/admin/routing/policies/:id — update policy
    • DELETE /api/v1/admin/routing/policies/:id — delete policy
  • Dry-run routing test. POST /api/v1/admin/routing/test accepts model + user_id + team_ids, evaluates active policies against all global provider configs with live health status, returns ranked candidates and the routing decision.
  • X-Switchboard-Provider response header. Every completion response includes providerID/configID for routing observability.
  • routing_decision column on usage_log. JSONB field for recording which policy matched, fallback depth, and total candidates evaluated.
  • RoutingPolicyStore interface with Postgres and SQLite implementations. Supports Create, Update, Delete, GetByID, ListActive, ListAll, ListForTeam.
  • 12 evaluator tests. Coverage: no policies, provider_prefer ordering, team_route filtering (including wrong-team no-op), model_alias rewrite, health skip-down, health sorting, all-down graceful degradation, priority ordering, empty candidates, inactive policy skip.

Changed

  • models/models.go: Added RoutingPolicy struct and RoutingDecision field on UsageEntry.
  • store/interfaces.go: Added RoutingPolicyStore interface and RoutingPolicies field on Stores.
  • store/postgres/stores.go, store/sqlite/stores.go: Wired RoutingPolicyStore into factory.
  • handlers/completion.go: Added router, healthStore fields. HealthStatusQuerier interface. evaluateRouting() method builds candidates from accessible configs, queries health status, runs evaluator, reloads credentials on config switch. X-Switchboard-Provider, X-Switchboard-Route, X-Switchboard-Fallback response headers.
  • main.go: Creates routing.Evaluator, wires into completion handler via SetRoutingEvaluator() and SetHealthStore(). Registers 6 admin routing routes.

Database

  • Postgres: server/database/migrations/014_v0222_routing.sql
  • SQLite: server/database/migrations/sqlite/013_v0222_routing.sql

[0.22.1] — 2026-03-02

Added

  • Provider profile schemas. Each provider type declares its configurable settings with types, defaults, validation constraints, and dependency rules. Built-in schemas for openai, anthropic, venice, openrouter. Unknown types fall back to the openai schema. providers/profile.go.
  • Provider hooks (pre-request / post-stream). Declarative request/response transforms driven by provider_configs.settings JSONB. Replaces any need for hardcoded provider switch statements in the completion path. providers/hooks.go.
    • OpenAI: system_prompt_prefix, frequency_penalty, presence_penalty → injected into ExtraBody.
    • Anthropic: extended_thinking + thinking_budget → injects thinking config into wire JSON, sets anthropic-beta header, clears temperature.
    • Venice: enable_thinking, enable_web_search, include_venice_system_prompt → builds venice_parameters in ExtraBody.
    • OpenRouter: route, require_parameters → serialized into X-Provider-Preferences header.
  • ExtraBody on CompletionRequest. Provider hooks write arbitrary key-value pairs that get merged into the wire-format JSON via mergeExtraBody() in both OpenAI and Anthropic doRequest methods. Keeps the canonical request type clean while supporting provider-specific extensions.
  • Anthropic extended thinking stream support. content_block_delta with type=thinking_delta now routes to the Reasoning field on StreamEvent, matching the existing reasoning_content path used by OpenAI-compatible providers.
  • Provider type registry. providers.RegisterType() combines provider implementation + metadata (name, description, default endpoint, profile schema). providers.ListTypes() returns all registered types.
  • GET /api/v1/admin/provider-types endpoint. Returns metadata and profile schemas for all registered provider types. Used by admin UI to render provider creation forms and show available settings per type.
  • Preset setting overrides. MergePresetSettings() merges persona-level overrides onto provider-level settings, respecting ProviderOnly fields that cannot be changed at the preset level.
  • Comprehensive tests. providers/hooks_test.go: 17 tests covering all four hook implementations, profile schemas, GetHooks, MergePresetSettings, mergeExtraBody, nil safety.

Changed

  • providers/registry.go: Init() now uses RegisterType() with full metadata instead of bare Register(). ProviderTypeMeta struct with ID, name, description, default endpoint, profile schema.
  • providers/provider.go: CompletionRequest.ExtraBody field (json:"-"). mergeExtraBody() helper function. Model struct unchanged.
  • providers/openai.go: doRequest() merges ExtraBody into wire JSON before HTTP dispatch.
  • providers/anthropic.go: doRequest() merges ExtraBody + sets anthropic-beta header when thinking is enabled. Stream processing handles thinking_delta content block deltas. Wire type gains Thinking field.
  • handlers/completion.go: PreRequest hook invoked at all three dispatch paths (multi-model, streaming, sync).
  • handlers/stream_loop.go: PostStreamEvent hook invoked after each streaming event in both streamWithToolLoop and streamModelResponse.
  • handlers/health_admin.go: Added GetProviderTypes handler.
  • main.go: GET /api/v1/admin/provider-types route registered.

[0.22.0] — 2026-03-02

Added

  • Provider health tracking. In-memory accumulator records success/error/timeout per provider config, flushes hourly buckets to provider_health table every 60s. Status derivation: healthy (<5% errors), degraded (525%), down (>25%). Background prune removes data older than 7 days.
  • Health admin endpoints. GET /api/v1/admin/providers/:id/health returns per-provider summary + hourly windows. GET /api/v1/admin/providers/health returns current-hour status for all providers.
  • Capability admin overrides. capability_overrides table supports per-provider or global overrides for any model capability field. Three-tier resolution chain: catalog → heuristic → admin override (highest priority).
  • Capability override endpoints. GET /api/v1/admin/models/:id/capabilities with source annotations (catalog/heuristic/override). PUT to set, DELETE to remove, GET /api/v1/admin/capability-overrides to list all.
  • Workspace pane layout. Frontend architecture replaces .chat-area + .side-panel with .workspace flex container. Primary and secondary panes are independent with drag-resize handle between them. Surfaces declare layout preferences via primary, secondary, secondaryOpts fields.

Fixed

  • scanJSON driver buffer aliasing (v0.21.7 bugfix). safe_json.go now copies the []byte from the database driver instead of aliasing the slice header. Fixes intermittent invalid character errors on multi-row JSON responses caused by driver buffer reuse between rows.Next() calls.
  • CreateChannel RETURNING path now uses scanJSON(&ch.Settings) instead of bare &ch.Settings scan.
  • UpdateChannel settings validationjson.Valid() check before JSONB merge, returns 400 on invalid JSON instead of storing corrupt data.

Changed

  • capabilities/intrinsic.go: ResolveIntrinsic() takes []models.CapabilityOverride as third parameter. applyOverrides() flips bool fields and sets int fields.
  • handlers/completion.go: HealthRecorder interface, recordHealth() called after every ChatCompletion call.
  • handlers/stream_loop.go: streamWithToolLoop and streamModelResponse accept configID + HealthRecorder params. Health recorded on error, timeout, and success paths.
  • store/interfaces.go: CapOverrides CapabilityOverrideStore added to Stores struct.
  • models/models.go: ProviderStatus, ProviderHealthWindow, ProviderHealthSummary, CapabilityOverride types.
  • main.go: Health accumulator lifecycle (start/stop/prune), admin route registration, startup log includes health status.
  • panels.js: Simplified to single _active panel in workspace secondary pane. Removed _dualMode, _splitRatio, _secondary.
  • surfaces.js: Layout declarations (primary, secondary, secondaryOpts) in surface registration.
  • editor-mode.js: Layout declarations for editor surface.
  • index.html: Workspace container structure, removed dual-view split button.
  • styles.css: Workspace pane CSS grid, handle, responsive rules.
  • app.js: _initWorkspaceResize() replaces _initSidePanelResize() + _initDualDivider().
  • ui-settings.js: Zoom selector targets updated class names.

Database

  • Migration 013 (Postgres) / 012 (SQLite): provider_health table (hourly bucketed metrics), capability_overrides table (admin corrections).

[0.21.6] — 2026-03-01

Added

  • Editor surface: document features merged in. Article mode (outline, export, word count, focus mode) folded into the single Editor surface rather than shipping as a separate surface. One surface, two concerns: code files get tabs + tree, text files get word count + export. Chat panel on the right with show/hide toggle.
  • New file button — Visible in both the file tree header (+ icon) and the editor toolbar ("+ New"). Creates files with smart defaults (markdown files get # Title scaffold).
  • Export dropdown — Download file, Export as HTML (via marked.js + DOMPurify), Copy to clipboard. Available from editor toolbar for any open file.
  • Chat panel toggle — Show/hide the right-side AI chat pane. Editor left pane expands to fill when chat is hidden. Toggle button in toolbar with active state indicator.
  • Word count + reading time in status bar for text files (markdown, txt, rst, adoc). ~230 wpm calculation.
  • Save indicator in status bar — shows "● Modified" with warning color for unsaved files.
  • Git status indicators in editor file tree (closing v0.21.5 deferred) — Tree rows show M/A/U badges with color coding (modified=yellow, added=green, untracked=italic gray, deleted=red strikethrough).
  • Auto-save on tab/mode switch in editor surface (closing v0.21.5 deferred) — Modified files auto-save when switching tabs or deactivating the editor surface.
  • Ctrl+Shift+F workspace search (closing v0.21.5 deferred) — Maps to Quick Open file finder.
  • SEED_PROVIDERS env var — Auto-creates global providers on startup from CSV format provider:api_key[:name]. Supports openai, anthropic, openrouter, venice, mistral, groq, together, fireworks, deepseek, perplexity with auto-filled endpoints. Idempotent. Blocked in production.
  • Mode selector labels — Surface buttons in sidebar show icon + label text ("Chat", "Editor"). Labels hide when sidebar collapsed.
  • Hash Router (router.js) — URL-driven direct-to-surface navigation:
    • #/chat → default chat view
    • #/chat/ch_abc123 → open specific chat (bookmarkable)
    • #/editor → editor surface (auto-picks workspace from project, or shows picker)
    • #/editor/ws_abc123 → editor with specific workspace Browser back/forward works. Replaces sessionStorage chat restore. Workspace picker overlay when navigating without a workspace. Auto-selects if only one exists.
  • openDirect(wsId) method on EditorMode — bypasses check() flow, registers surface directly with a given workspace ID. Used by Router for hash-based navigation.

Fixed

  • chat.switched / chat.created events never emittedselectChat(), newChat(), and sendMessage() now emit these events. This was blocking EditorMode from detecting workspace bindings on channel switch.
  • workspace_id missing from channel responses — Added to all SELECT/RETURNING/Scan paths for both SQLite and Postgres. Frontend chat objects now include workspace_id.
  • EditorMode.check() optimization — Checks local App.chats data before API calls.
  • defaultEndpoints redeclared — Renamed to seedDefaultEndpoints in seed_providers.go to avoid collision with live_provider_test.go.

Removed

  • Article surface (article-mode.js, article-mode.css) — Merged into Editor. Document features (export, word count, focus mode) are now part of the unified Editor surface. Article-specific AI tools (suggest_outline, expand_section) deferred to v0.22+ as editor extensions.

Changed

  • editor-mode.js: Rebuilt header with New/Export/ChatToggle buttons. File tree has header with + button. Chat panel show/hide. Word count + save indicator in status bar. _createNewFile(), _exportActiveFile(), _toggleChat() methods. openDirect() for Router.
  • editor-mode.css: Header labels, export dropdown, chat toggle, file tree header, save indicator, word count styles.
  • chat.js: Event emissions, workspace_id in chat objects, hash sync via Router.update().
  • app.js: Router initialization replaces sessionStorage restore.
  • projects-ui.js: Project list mapping includes workspace_id.
  • channels.go: workspace_id in all query/scan paths.
  • surfaces.js: Updated comment (removed article reference).
  • styles.css: Mode selector layout, router picker overlay.
  • index.html: Removed article-mode.css/article-mode.js, added router.js.
  • config.go: SeedProviders field.
  • main.go: SeedProviders() call.
  • seed_providers.go: seedDefaultEndpoints (renamed from defaultEndpoints).
  • ROADMAP.md: Article→Editor merge documented. Go template pages added under v0.25.0.

[0.21.5] — 2026-03-01

Added

  • Editor Surface (Development Mode). IDE-like experience built as a v0.21.3 surface consuming workspace primitives. Activates automatically when a channel or project has a bound workspace. Three-pane layout: file tree (sidebar), code editor (CM6), and AI chat panel with resizable split.
  • editor-mode.js — Full editor surface module. File tree backed by workspace_ls API with nested directory expansion, file icons by extension, and context menus. Tab bar with modified indicators, close buttons, and multi-file editing. CM6 integration with language auto-detection (20+ languages) and textarea fallback when CM6 bundle unavailable.
  • Split pane layout with draggable resize handle. Editor (left) and AI chat (right) share the main region. Chat DOM borrowed from Surfaces saved fragments — real conversation history and input preserved across mode switches.
  • Quick Open (Ctrl/Cmd+P): Fuzzy file finder overlay using workspace file index. Ctrl/Cmd+S save, Ctrl/Cmd+W close tab.
  • Live editor updates: When AI tool calls modify workspace files (workspace_write, workspace_patch), the server emits workspace.file.changed via WebSocket. If the file is open and unmodified in the editor, content refreshes automatically.
  • Status bar: Current file path, language mode, git branch display.
  • editor-mode.css: Complete styling for all editor components with dark theme support, mobile responsive (stacked layout at ≤768px).
  • Workspace API methods on frontend API module: getWorkspace, listWorkspaceFiles, readWorkspaceFile, writeWorkspaceFile, deleteWorkspaceFile, mkdirWorkspace, getWorkspaceGitStatus, getWorkspaceGitBranches.
  • Surfaces.getSavedFragment() / putSavedFragment() — Cross-surface DOM sharing. Allows the editor surface to borrow chat DOM into its split pane without cloning.
  • workspace.file. event routing added to server event route table (DirToClient).
  • Workspace management UI: Create, list, and bind workspaces from the frontend. Project settings panel gains a Workspace section with dropdown picker and "New" button. Chat context menu gains "Set workspace…" option for per-channel override. Both paths support creating new workspaces inline.
  • GET /workspaces — List all workspaces accessible to the current user (user-owned + team-owned).
  • workspace_id in channel API: updateChannelRequest accepts workspace_id for binding/unbinding. channelResponse and both list/get queries now include workspace_id.

Changed

  • stream_loop.go: Emits workspace.file.changed event after successful workspace_write/workspace_patch tool calls.
  • index.html: Added editor-mode.css stylesheet and editor-mode.js script.
  • channels.go: channelResponse includes workspace_id, list/get queries select it, update accepts it.
  • workspaces.go: Added List handler for GET /workspaces.
  • main.go: Added GET /workspaces route.
  • projects-ui.js: Workspace section in project panel, workspace picker in chat context menu.
  • styles.css: Workspace row layout in project panel.

[0.21.4] — 2026-03-01

Added

  • Git Integration. Workspaces can now track a git remote. Operations use os/exec calling the git binary (no CGO, no Go git libraries). Credentials are vault-encrypted (AES-256-GCM) using the same pattern as BYOK provider configs.
  • git_credentials table. Postgres + SQLite migrations. Stores encrypted PAT tokens, HTTP basic auth, and SSH private keys. Columns: id, user_id, name, auth_type, encrypted_data, nonce, created_at.
  • Workspace git columns. git_remote_url, git_branch, git_credential_id, git_last_sync added to workspaces table via migration.
  • workspace.GitOps — exec-based git operations: Clone, Pull, Push, Status, Diff, Commit, Log, BranchList, Checkout. Credential injection via temporary GIT_ASKPASS scripts (PAT), .git-credentials files (basic auth), or GIT_SSH_COMMAND (SSH keys). Temp files securely wiped after each operation.
  • Post-operation hooks. Clone, pull, and checkout automatically trigger workspace reconcile (filesystem → DB sync) and re-index changed files through the v0.21.2 indexing pipeline.
  • 5 git tools for LLM use: git_status, git_diff, git_commit, git_log, git_branch. Filtered out when no workspace is bound. Each validates git remote configuration before execution.
  • 9 git API endpoints: POST clone, POST pull, POST push, GET status, GET diff, POST commit, GET log, GET branches, POST checkout — all under /api/v1/workspaces/:id/git/.
  • 3 credential endpoints: POST /api/v1/git-credentials (create), GET /api/v1/git-credentials (list user-scoped summaries), DELETE /api/v1/git-credentials/:id (delete). Encrypted data never exposed in API responses.
  • GitCredentialStore interface + Postgres/SQLite implementations. Create, GetByID, ListByUser, Delete with user-scoped authorization.
  • Security. file:// and local path URLs rejected in clone. Error output sanitized to strip credential-containing lines. GIT_TERMINAL_PROMPT=0 set on all operations to prevent interactive prompts.

Changed

  • WorkspaceStore interface: added SetGitLastSync() method + both implementations.
  • models.Workspace: added git tracking fields. models.WorkspacePatch: added git patch fields.
  • Completion handler: git tools disabled alongside workspace tools when no workspace bound.
  • store.Stores struct: added GitCredentials field.
  • Postgres/SQLite NewStores(): wires GitCredentialStore.

[0.21.3] — 2026-03-01

Added

  • Surface Registry (surfaces.js). Mode-switching system that allows extensions to register "surfaces" (UI modes) that take over named regions of the interface. DOM nodes are detached and preserved in DocumentFragments during mode switches — not destroyed — ensuring CM6 editor instances and other stateful components survive transitions.
  • Surface region attributes. Four data-surface-region attributes on index.html containers: surface-header (model bar), surface-main (chat messages), surface-footer (input area), sidebar-content (search + chat history). Extensions swap these regions via ctx.ui.replace() / ctx.ui.restore().
  • Mode selector. Appears in the sidebar (#modeSelectorWrap) when ≥1 extension surface is registered beyond the default chat. Shows icon buttons for each mode with active state highlighting.
  • Extension context API. ctx.surfaces.register(), ctx.surfaces.unregister(), ctx.surfaces.activate(), ctx.surfaces.getCurrent() on the scoped extension context. ctx.ui.replace() and ctx.ui.restore() for region DOM management.
  • Surface events. surface.activated, surface.deactivated, surface.registered, surface.unregistered on the EventBus (all localOnly).
  • REPL console (repl.js). Fourth tab in the debug modal (Ctrl+Shift+L). AsyncFunction wrapper for top-level await. Injected globals: API, Events, Extensions, Surfaces, DebugLog, Panels, UI, plus $(), $$(), sleep() helpers. Features: collapsible JSON pretty-printing, red errors with expandable stack traces, command history via ↑/↓ (persisted to sessionStorage), tab-completion on live object graphs and EventBus labels, multi-line input (Shift+Enter), admin-gated (admin role OR ?debug=1 URL param).
  • CSS. Mode selector styles (.mode-selector, .mode-btn, active/hover states). REPL styles (output formatting, value-type color coding, collapsible JSON, input prompt).

Changed

  • extensions.js context builder: added ctx.surfaces namespace and ctx.ui.replace() / ctx.ui.restore() methods.
  • app.js startup: initializes Surfaces.init() before Extensions.loadAll(), and REPL.init() after auth confirmation.
  • index.html: added data-surface-region attributes, mode selector container, REPL tab in debug modal, script tags for surfaces.js and repl.js.
  • EXTENSIONS.md §6: updated with implementation details, actual API signatures, and event catalog.

[0.21.2] — 2026-03-01

Added

  • Workspace Indexing Pipeline. Background text chunking and embedding for workspace files, reusing the existing knowledge.SplitText chunker and knowledge.Embedder for vector generation. Indexable file types include 40+ source code extensions (Go, Rust, Python, TypeScript, Perl, etc.) plus all text/* MIME types. Code-aware chunking uses 1500-character chunks with function/class/type boundary separators (\n\nfunc , \n\nfn , \n\nclass , \n\ndef , \n\ntype , \n\nsub , \n\npub , \n\nimpl ), falling back to paragraph and line breaks.
  • Content-addressed skip. SHA256 computed during WriteFile is compared against the prior hash before triggering re-indexing. Unchanged files skip the chunk + embed pipeline entirely, making archive extracts and bulk writes efficient.
  • workspace_chunks table. New table (Postgres + SQLite migrations) storing text chunks with vector embeddings per workspace file. Postgres uses sequential scan with pgvector <=> cosine operator (no IVFFlat/HNSW index — 3072-dim embeddings exceed the 2000-dim index limit; sequential scan is adequate at workspace scale). SQLite uses app-level cosine in Go. Columns: workspace_id, file_id, chunk_index, content, token_count, embedding, metadata (JSONB with line_start, language).
  • workspace_files additions. index_status column (pending, indexing, ready, error, skipped) and chunk_count for tracking indexing progress per file. workspaces table gains indexing_enabled boolean (default true).
  • WorkspaceStore chunk methods. Four new store interface methods implemented for both Postgres and SQLite: InsertChunks (batch insert with pgvector cast or JSON text), DeleteChunksByFile, SimilaritySearch (pgvector <=> operator or app-level cosine), UpdateFileIndexStatus.
  • workspace.Indexer. Background indexing orchestrator with configurable concurrency semaphore. Single-file indexing (IndexFile) triggered on WriteFile when content changes. Batch indexing (IndexBatch) triggered after ExtractArchive. Async goroutines with 5-minute timeout per file. Embedding usage tracked via Embedder.LogUsage.
  • workspace_search tool. Semantic search across workspace files via natural language query. Embeds the query, runs cosine similarity against workspace chunks, returns file paths, content snippets, relevance scores, and approximate line numbers. Optional file_pattern glob filtering (e.g. *.go, src/*.ts) applied post-search. Top-K capped at 20.
  • Indexer integration. workspace.FS.SetIndexer() hooks the indexer into the write path. WriteFile triggers single-file indexing on content change. ExtractArchive triggers batch indexing for all extracted files. Both use the workspace owner's identity for embedding provider resolution.
  • Configuration. WORKSPACE_INDEXING_ENABLED env var (global kill switch, default true). WORKSPACE_INDEX_CONCURRENCY env var (default 2). Per-workspace indexing_enabled toggle via WorkspacePatch. Indexing gracefully degrades when no embedding role is configured.
  • Index status endpoint. GET /api/v1/workspaces/:id/index-status returns aggregated indexing progress: per-status file counts (pending, indexing, ready, error, skipped), total chunk count, and workspace indexing_enabled flag.

Changed

  • workspace_files scanner updated in both Postgres and SQLite stores to include index_status and chunk_count columns.
  • workspaces scanner updated to include indexing_enabled column.
  • WorkspacePatch model extended with IndexingEnabled field.
  • WorkspaceToolNames() now includes workspace_search in the disabled set when no workspace is bound.
  • RegisterWorkspaceSearchTool() added for late registration of the search tool (requires embedder).

[0.21.1] — 2026-03-01

Added

  • Workspace Tools. Six AI-callable tools in the workspace category: workspace_ls (list files with content type/size), workspace_read (text content with 50KB soft limit, binary detection), workspace_write (create/overwrite with auto-mkdir), workspace_rm (delete with recursive guard), workspace_mv (move/rename with auto-mkdir), workspace_patch (find/replace operations, each match must be unique, 1MB file limit). Late registration via RegisterWorkspaceTools() after stores + FS init. WorkspaceToolNames() returns the full list for filtering.
  • Channel Workspace Binding. channels.workspace_id nullable FK column (Postgres + SQLite migration 010/009). Channels with a bound workspace get workspace tools auto-injected into the completion tool set.
  • Project Workspace Binding. projects.workspace_id nullable FK column. Projects can bind a workspace shared by all channels in the project.
  • Workspace Resolution Chain. ChannelStore.ResolveWorkspaceID() resolves the effective workspace for a channel: channel workspace_id takes priority, falling back to the project's workspace_id via project_channels join. Both Postgres and SQLite implementations. Returns empty string when no workspace is bound.
  • Tool Injection in Completion Handler. buildToolDefs() accepts workspaceID parameter; when empty, all workspace tool names are added to the disabled set. ExecutionContext.WorkspaceID field carries the resolved workspace ID through tool execution and streaming.
  • Workspace resolution wired into streaming. Both CompletionHandler and streamWithToolLoop resolve the workspace ID from the channel and pass it through to tool execution contexts.

Deferred

  • Frontend workspace UI (channel settings toggle, project Files tab, chat bar workspace indicator) deferred to a future release.
  • Archive-to-workspace upload flow (detect archive MIME + workspace binding → extract to workspace) deferred.

[0.21.0] — 2026-03-01

Added

  • Workspace Storage Primitive. Platform-level file storage bound to users, projects, channels, or teams via polymorphic owner model. Dual-layer architecture: PVC filesystem (source of truth) with DB metadata index (queryable cache). Workspaces support configurable quotas, status lifecycle (active/archived/deleting), and owner-based authorization inheritance.
  • Workspace data model. Two new tables: workspaces (polymorphic owner_type/owner_id, root_path, max_bytes quota, status) and workspace_files (path, MIME type, size, sha256, is_directory). Unique index on (workspace_id, path) enables upsert-on-conflict for file metadata sync. Migrations for both Postgres and SQLite.
  • workspace.FS package. Filesystem operations layer with security-first design: atomic writes (temp file + rename with SHA256 computed via tee reader), path traversal guards (cleanPath normalization + absPath containment validation), symlink rejection on write targets. Operations: ReadFile, WriteFile, DeleteFile (with recursive guard), Mkdir, Stat, ListDir, Tree, Reconcile (filesystem→DB drift sync).
  • Archive operations. Extract zip and tar.gz archives into workspaces with bomb protection: 10K file limit, 100MB single file cap, workspace quota enforcement during extraction. Common-prefix stripping handles GitHub-style project-name/ wrapper directories. CreateArchive packages workspaces into downloadable zip or tar.gz.
  • Content type detection. Extension-based MIME detection covering 40+ source code types (Go, Rust, Python, TypeScript, etc.) with http.DetectContentType sniffing fallback for unknown extensions.
  • WorkspaceStore interface. Full CRUD for workspaces, file index operations (upsert, delete, delete-by-prefix, get, list with recursive/non-recursive modes), ownership lookup (GetByOwner, ListByOwner), and aggregate stats. Postgres and SQLite implementations.
  • Workspace API. 15 new endpoints under /api/v1/workspaces: workspace CRUD (create, get, update, delete), file operations (list, read, write, delete, mkdir), archive management (upload with extraction, download), reconcile (FS→DB sync), stats. Owner-based authorization: user workspaces require self, channel workspaces require channel owner, project workspaces require project member, team workspaces require team member.
  • Unit tests. Path cleaning (13 cases including traversal, dotfiles, whitespace), absPath traversal detection, MIME detection (14 extensions), unsafe path filtering (7 cases), common prefix detection (5 cases), write/read round-trip with mock store, delete verification, mkdir with index sync.

[0.20.0] — 2026-03-01

Added

  • Notifications Core (Phase 1): Persistent, user-targeted notification infrastructure with real-time WebSocket delivery. notifications table (Postgres + SQLite), NotificationStore with paginated queries, Service.Notify() / NotifyMany() for centralized creation and dispatch. Five API endpoints: list (paginated, filterable), unread count, mark read, mark all read, delete. Bell icon in header bar with unread count badge (capped at 9+). Notification dropdown (latest 10, grouped, click-to-navigate via resource_type/resource_id). Full notification panel registered with PanelRegistry. WebSocket push via notification.new event with toast for high-priority types (kb.error, role.fallback). Background cleanup goroutine (configurable retention, default 90 days). Initial sources: role.fallback (via EventBus subscription), kb.ready/kb.error (knowledge base processing), grant.changed (group membership).
  • @mention Parsing + Multi-model Routing (Phase 2): Channels support multiple AI models with @mention-based routing. mentions.Parse() extracts @mentions from message content, resolves against channel model roster (case-insensitive, longest-match-first, trailing punctuation tolerant). Completion handler fans out sequentially to mentioned model(s), producing one assistant response per target. Without @mention, default channel model responds (fully backward compatible). Channel model CRUD: 4 new endpoints for add/remove/update/list. Frontend: model pills in chat header, @mention autocomplete (CM6 mentionCompletion extension with roster-backed suggestions), model attribution labels on multi-model responses. Message model_display field for human-readable attribution. SSE streaming tagged with model info per response.
  • Email Transport + Notification Preferences (Phase 3): SMTP email delivery via EmailTransport supporting implicit TLS (port 465) and STARTTLS (port 587). Multipart MIME messages (HTML + plaintext) with branded templates using Go html/template. notification_preferences table with three-tier resolution: specific type → user wildcard * → system default (in_app=true, email=false). Three preference API endpoints (list, set with partial patch, delete). Admin SMTP configuration in settings (host, port, user, password, from address, TLS mode) with test email endpoint. User notification preferences UI in Settings → Notifications tab with per-type in-app/email checkboxes. Async email delivery (goroutine with 30s timeout, failures logged non-blocking).
  • Gin Release Mode: Backend now automatically sets gin.SetMode(gin.ReleaseMode) when ENVIRONMENT=production, suppressing per-request access logs for health checks and reducing log noise. GIN_MODE env var also added to K8s backend deployment as explicit override.

Fixed

  • Model preferences 500 on every page load: GET /api/v1/models/preferences returned HTTP 500 due to NULL values in user_model_settings.hidden and sort_order columns failing Go sql.Scan() into non-pointer types. Root cause: the Set upsert passed NULL for unset patch fields, bypassing DEFAULT false/DEFAULT 0 on INSERT. Fixed with COALESCE in both SELECT (read path) and INSERT VALUES (write path) for Postgres and SQLite. Includes data-fix SQL for existing NULL rows.

[0.19.2] — 2026-02-28

Added

  • Project Persona Default: Bind a persona to a project via the detail panel. All chats in the project inherit the persona's model, parameters, and system prompt as a fallback when no explicit preset is selected. Resolution chain: explicit request → project persona → none.
  • Project Archive Toggle: Archive/unarchive projects from the detail panel. Archived projects hidden from sidebar by default with "Show archived (N)" toggle. Dimmed visual treatment when shown.
  • Channel Reorder: Right-click a chat within a project for "Move up" / "Move down" options. Server-persisted positions via project_channels.position, loaded on startup, maintained across moves.

[0.19.1] — 2026-02-28

Added

  • Active Project: Pin a project as active; new chats auto-assign to it. Persists across reloads via localStorage. Visual indicator (📌 + accent border) in sidebar.
  • Project System Prompt: Per-project instructions stored in projects.settings JSONB. Injected between persona/channel prompt and KB hint during completion. Merge semantics on update (preserves other settings keys).
  • Project Detail Panel: Side panel (via PanelRegistry) for managing project system prompt, KB bindings, and notes. Accessible from project ⋯ menu → "Project settings".
  • Enriched ListKBs and ListNotes responses with JOIN-sourced name/title fields for display in the project panel.

Changed

  • ProjectPatch model now accepts settings field for partial settings merge.
  • Project context menu expanded: "Pin as active", "Project settings", plus existing rename/color/delete.

[0.19.0] — 2026-02-28

Added

  • Projects / Workspaces. Organizational containers that group related conversations, knowledge bases, and notes into a single workspace. Projects provide a scope-aware organizational layer above individual channels, with support for personal, team, and global visibility.
  • Project data model. Four new tables: projects (with scope, owner, team, color, icon, settings JSONB), project_channels (ordered membership with UNIQUE constraint), project_knowledge_bases (with auto_search flag), and project_notes. Channels gain a denormalized project_id FK for efficient filtering.
  • Project API. 17 new endpoints under /api/v1/projects: full CRUD, channel add/remove/list/reorder, KB add/remove/list, note add/remove/list. Access checks enforce owner-or-team-member visibility with owner-only delete.
  • Project KB resolution. Knowledge bases bound to a project are automatically available to all channels within that project. Resolution chain extended: Persona KBs → Project KBs → Channel KBs → Personal KBs. Both BuildKBHint (system prompt injection) and kbsearch (tool-time retrieval) updated.
  • Note auto-association. Notes created from a channel that belongs to a project are automatically added to that project's note collection.
  • Sidebar project groups. Projects appear as collapsible groups in the sidebar above the existing time-based "Recent" section. Each group shows a color dot, chat count, and an options menu (⋯) for rename, color picker, and delete.
  • Drag-and-drop. Drag chat items between project groups or back to Recent. Visual feedback with outline and background highlight on valid drop targets.
  • Right-click context menu. Right-click any chat to move it to a project, remove it from its current project, or create a new project and assign in one step.
  • New Project button. Added to the New Chat split-button dropdown for quick project creation.
  • Channel project_id filter. GET /api/v1/channels accepts ?project_id=<uuid> to filter by project, or ?project_id=none for unassigned channels. Response includes project_id field.

Changed

  • renderChatList() rewritten to support project grouping while preserving the original time-based layout when no projects exist. Chat items now include draggable, oncontextmenu, and drag event handlers.
  • Channel response struct, SELECT queries, and scan calls updated across ListChannels, GetChannel, and CreateChannel (both Postgres and SQLite paths) to include project_id.
  • resource_grants CHECK constraint extended to include 'project' as a valid resource type.
  • db-validate.sh updated: former "Dropped tables" assertions for projects and project_channels replaced with positive checks for all four project tables plus channels.project_id column check.
  • Test helper truncation list updated with project junction tables.

Fixed

  • JSON corruption defense (hotfix carry-forward from 0.18.2): SafeJSON wrapper with scanJSON and scanTags hardened helpers that replace bare json.RawMessage / pq.Array scanning. Prevents encoding/json: invalid character panics from NULL or empty columns.
  • Service worker (hotfix carry-forward): chrome-extension:// URL filtering to avoid opaque response cache errors.

[0.18.1] — 2026-02-28

Added

  • Side panel architecture. Complete rewrite of the side panel system from shared-tab layout to independent single-slot panels. Any action (preview, notes, diagram pop-out) fills the slot, replacing whatever was there — no tabs, no association between panel types.
  • Panel registry (PanelRegistry): named panels with independent open/close state, scroll/state preservation across switches, keyboard shortcuts (Ctrl+\ cycle, Ctrl+Shift+\ toggle dual-view).
  • Dual-view mode: two panels side-by-side via CSS grid with a drag-adjustable split ratio (0.20.8 range, 6px divider handle). Toggle button in header actions area alongside fullscreen and close.
  • Live HTML preview: streaming responses update the preview iframe in real-time (500ms debounce). Extracts the last fenced HTML block from partial content and renders it as the response streams in.
  • Extension pop-out: button on rendered extension blocks (mermaid, KaTeX, etc.) clones the content into the preview iframe and opens the side panel.
  • Extension UI primitives (ctx.ui): seven methods exposed to browser extensions through the scoped extension context:
    • toast(msg, type) — toast notifications via UI.toast()
    • openPreview(html) — load HTML into side panel preview iframe
    • isDark() — theme detection without DOM sniffing
    • isMobile() — viewport width check (≤768px)
    • isPanelOpen() — side panel container visibility
    • confirm(msg, opts) — modal confirm dialog (Promise<boolean>)
    • createMenu(anchor, opts) — popup menu (unchanged from stub)
  • Mermaid context-aware expand: single button replaces the previous two-button (pop-out + fullscreen) approach. When the side panel is open, expand pops the diagram into it; when closed, expand goes fullscreen.
  • Mermaid fullscreen close button: 40px circular close button overlaid top-right in fullscreen mode, 48px on mobile. Fixes the previous Escape-key-only exit which was unusable on touch devices.
  • Mobile side panel: swipe navigation between panels (80px threshold, 1.5× horizontal-to-vertical ratio), tap-to-close overlay, responsive auto-collapse of dual mode on narrow viewports, enlarged touch targets for all panel controls.
  • Side panel header label: simple text label showing the active panel name, replacing the previous tab bar UI.

Changed

  • Side panel model changed from tabbed (Preview + Notes tabs visible simultaneously) to single-slot (one panel fills the space, actions replace it). No user-selectable tabs — content is entirely action-driven.
  • Dual-view toggle button moved from tab bar (dynamically injected) to static header actions area next to fullscreen and close.
  • Mermaid extension refactored to use ctx.ui primitives exclusively. Zero direct references to UI.*, PanelRegistry.*, or DOM class sniffing for theme detection. Extension remains fully self-contained in extensions/builtin/mermaid-renderer/.
  • Mermaid source copy uses ctx.ui.toast() instead of inline button text swap. Theme detection uses ctx.ui.isDark() instead of manual document.body.classList.contains('dark-theme') check.
  • Side panel outer resize minimum bumped to 480px in dual mode (vs 280px single).

Removed

  • Tab bar UI (.side-panel-tabs, .side-panel-tab, tab rendering logic). Panels no longer have user-selectable tabs.
  • Separate pop-out and fullscreen buttons in mermaid toolbar (collapsed into single context-aware expand button).

[0.18.0] — 2026-02-28

Added

  • Memory system. Long-term memory across conversations with scope-aware isolation. Three memory scopes: user (personal facts/preferences), persona (shared across all users of a Persona), and persona_user (per-user within a Persona context, e.g. tutoring progress per student). Memories persist across channels and are injected into the system prompt at completion time with scope priority (persona_user > persona > user).
  • Memory tools. Two new LLM-callable tools:
    • memory_save — LLM explicitly stores a fact with key/value/confidence. Scope-aware: saves to the active scope for the current Persona context.
    • memory_recall — LLM queries stored facts relevant to current context. Merges results from applicable scopes with semantic search via embeddings and keyword fallback.
  • Automatic memory extraction. Background scanner finds conversations with sufficient new activity, sends them to the utility model role for fact extraction, and stores results as pending_review memories. Configurable extraction prompt per Persona (e.g. "extract FAQ-worthy Q&A pairs"). Scanner runs on a configurable interval with concurrency control. Global kill switch via admin settings.
  • Memory review pipeline. Extracted memories start in pending_review status. Admin panel shows pending review queue with bulk approve. Users can approve/reject/edit their own pending memories from the Settings → Memory tab.
  • User memory management. Settings → Memory tab shows active/pending memory counts, filterable/searchable memory list with inline edit and delete. Status filter (active, pending_review, archived). Approve All button for batch operations on pending memories.
  • Admin memory controls. Admin panel → Memory section shows system-wide pending review queue. Memory extraction toggle in admin Settings panel (memory_extraction_enabled). Bulk approve endpoint for batch review.
  • Hybrid semantic recall. Memory recall supports both keyword search and vector similarity (cosine distance). Postgres uses <=> operator with pgvector; SQLite computes cosine similarity in Go with app-level vector loading. Results are merged and deduplicated.
  • Memory injection at completion time. BuildMemoryHint() loads relevant memories and formats them as a system prompt section, injected after the knowledge base hint. Context-budget aware with configurable character limit. Supports embedding-based relevance filtering when the user's latest message is available.
  • Persona memory configuration. Personas gain memory_enabled (bool) and memory_extraction_prompt (text) fields. When memory is enabled on a Persona, conversations with that Persona contribute to persona-scoped memory extraction. Custom extraction prompts allow specialization (e.g. helpdesk FAQ extraction vs. tutoring progress tracking).
  • Database migrations. Four new migration files (Postgres + SQLite):
    • 004_v0180_memories.sql / sqlite/003_v0180_memories.sqlmemories table with composite unique index, full-text search index (GIN/keyword), memory_extraction_log tracking table.
    • 005_v0180_memory_phase2.sql / sqlite/004_v0180_memory_phase2.sql — Persona memory columns, extraction log unique constraint.
  • API endpoints. Eight new authenticated endpoints:
    • GET /memories — list user's memories (filterable by status/query)
    • GET /memories/count — active + pending counts
    • PUT /memories/:id — edit a memory's key/value/confidence
    • DELETE /memories/:id — delete a memory
    • POST /memories/:id/approve — approve a pending memory
    • POST /memories/:id/reject — reject (archive) a pending memory
    • GET /admin/memories/pending — admin pending review queue
    • POST /admin/memories/bulk-approve — admin bulk approve

Changed

  • CompletionHandler now accepts an *knowledge.Embedder parameter for memory injection with semantic relevance filtering.
  • Persona create/update forms include memory configuration fields (enabled toggle, extraction prompt textarea).
  • Admin settings save handler includes memory_extraction and memory_extraction_enabled keys.
  • store.Stores struct includes Memories MemoryStore field.
  • Admin panel sections include Memory with pending review loader.

Technical Notes

  • Memory embeddings use vector(3072) matching the existing KB/notes schema. HNSW index is not used (pgvector limits HNSW to 2000 dims); filtered sequential scans are performant for per-user memory tables. IVFFlat or dimension reduction available as future optimizations.
  • SQLite hybrid recall loads embeddings into Go and computes cosine similarity at the application level, reusing the existing cosineSimilarity() function from the knowledge base store.
  • Memory tools use late registration (like KB search and note tools) because they require stores and embedder dependencies initialized in main.go.

[0.17.3] — 2026-02-28

Added

  • Wikilink bi-directional linking. Notes support [[Title]] and [[Title|display text]] syntax. Links are extracted on save via regex, resolved to target note IDs by case-insensitive title match, and stored in a note_links junction table. Dangling links (references to notes that don't exist yet) are preserved and automatically resolved when a matching note is created later.
  • Transclusion embeds. ![[Title]] syntax renders embedded note content inline in read mode. Content is fetched asynchronously with a recursion guard (max depth 1 — nested transclusions render as plain text references). Transclusion links are visually distinct in both the editor (border-left, italic) and the graph (dashed edges).
  • Backlinks panel. Read mode shows a collapsible "Linked mentions" panel below the note content listing all notes that link to the current note. Each backlink is clickable to navigate directly. Count badge updates on every note open.
  • Knowledge graph visualization. Canvas-based force-directed graph with no external dependencies (~480 lines). Physics: O(n²) Coulomb repulsion, Hooke spring attraction on edges, center gravity. Interaction: pan, zoom (0.154.0x toward cursor), drag nodes, hover highlights node + neighbors. Click opens note editor. Nodes sized by √(link_count), colored by folder path (10-color palette). Ghost nodes for unresolved [[links]] with toggle button. Energy-based pause (stops rAF when total kinetic energy falls below threshold). ResizeObserver for responsive canvas sizing.
  • CM6 note editor. New CM.noteEditor() factory function with live markdown preview (heading sizes for h1h3, blockquote styling, fenced code block decorations), [[wikilink]] autocomplete triggered by [[ with async title search, and clickable wikilink chip rendering. Falls back to plain <textarea> if CM6 bundle is unavailable.
  • Wikilink autocomplete. Typing [[ in the note editor triggers an async completion popup backed by GET /notes/search-titles?q=. Results are fetched via ILIKE title match, limited to 8 suggestions. Selecting a result inserts the full [[Title]] syntax.
  • Daily notes. "Today" button in the notes toolbar creates or opens a daily note titled Daily — YYYY-MM-DD in the /daily/ folder with a starter template (Tasks + Notes sections). Idempotent — repeated clicks navigate to the existing daily note.
  • Save-to-note from chat. "Note" button in message action bar captures the full message content (or the current text selection within that message) into a new note with pre-filled title extracted from the first line. Provenance is tracked via source_message_id column on the notes table, enabling future jump-to-source navigation.
  • API endpoints. Three new authenticated endpoints:
    • GET /notes/search-titles?q=&limit= — lightweight title search for autocomplete (returns [{id, title}]).
    • GET /notes/:id/backlinks — notes linking to the specified note, with display text and metadata.
    • GET /notes/graph — full graph topology: nodes (with inbound/outbound link counts), resolved edges, and unresolved (dangling) link titles.
  • note_links table. New junction table with source_note_id, target_note_id (nullable for dangling links), target_title, display_text, is_transclusion, and created_at. Primary key on (source_note_id, target_title). Index on target_note_id for backlink queries. Migrations for both Postgres and SQLite.
  • source_message_id column. New nullable UUID column on notes table for chat-to-note provenance tracking.
  • Wikilink extraction package. Standalone notelinks/ package with regex-based parser handling [[Title]], [[Title|Display]], ![[Title]], and ![[Title|Display]]. Deduplicates by lowercase title, preserves first occurrence. 10 unit tests covering edge cases.

Changed

  • Notes editor replaces <textarea> with CM6 noteEditor instance. The container #noteEditorContentContainer is lazily initialized on first edit; destroyed on panel close to avoid stale state.
  • Note create and update handlers now extract wikilinks from content and call ReplaceLinks() (transactional DELETE + batch INSERT). Create also calls ResolveByTitle() to fix dangling links from other notes.
  • showNotesList() destroys the CM6 editor instance and hides graph view.
  • saveNote() and deleteNote() call invalidateNoteGraph() to clear the cached graph data.
  • Read mode renders [[wikilinks]] as styled clickable chips via post- processing of the formatted HTML. Clicking navigates to the linked note or offers to create it if not found.
  • ARCHITECTURE.md updated with notelinks/ package, note-graph.js, CM.noteEditor() factory, and expanded notes description covering the linking model.
  • ROADMAP.md updated with full v0.17.3 section including all checklist items.
  • CM6 bundle entrypoint (index.mjs) exports noteEditor alongside existing chatInput and codeEditor.
  • theme.mjs adds noteEditorTheme with full-height layout (min 200px, max 60vh), heading size decorations, and blockquote styling.

[0.17.2] — 2026-02-28

Added

  • CodeMirror 6 integration. Rich editor infrastructure compiled at Docker build time via esbuild (IIFE bundle, ~295KB min / ~90KB gzip). Two factory functions exposed on window.CM:
    • CM.chatInput() — Markdown-mode editor for the chat input with auto-growing height, Enter=send / Shift+Enter=newline, spell check, and WYSIWYG fenced code block decorations (visual container with monospace font and accent border, matching claude.ai UX).
    • CM.codeEditor() — Full-featured code editor for admin extension panel with line numbers, bracket matching, search/replace, fold gutter, and 10 bundled language modes (Markdown, JavaScript, JSON, SQL, HTML, CSS, YAML, Go, Python, Rust).
  • Graceful degradation. All CM6 integration points check window.CM availability. If the bundle fails to load, the app falls back to native <textarea> with zero breakage.
  • Dark/Light/System theme toggle. New appearance setting with three modes. Light theme overrides all CSS variables via [data-theme="light"] selector. System mode tracks prefers-color-scheme media query in real time. Theme changes emit theme.changed on the EventBus; CM6 editors toggle oneDark syntax theme via compartment reconfiguration.
  • Vim/Emacs keybinding preference. Editor keybinding mode (Standard / Vim / Emacs) configurable in appearance settings. Applies to code editors and extension editors only — chat input always uses standard keybindings. Live-switchable on already-open editors via keymap.changed event. Bundled statically (~40KB for both modes).
  • Inline code shortcut. Ctrl/Cmd+E wraps selection in backticks or inserts an empty inline code pair with cursor between them.
  • Code block shortcut. Typing ``` at the start of a line expands to a fenced code block with cursor positioned inside. The decoration plugin renders code blocks with a styled visual container in the chat input.
  • CI path-based change detection. New detect-changes job classifies changed files into frontend/backend/infra/docs buckets. Test jobs skip when irrelevant (FE-only changes skip Go tests, docs-only changes skip all tests and deploy). Tags always run the full pipeline.

Changed

  • Docker build pipeline: both Dockerfile.frontend and unified Dockerfile now include a cm6-build stage (Node 20 Alpine → esbuild → IIFE bundle).
  • ChatInput abstraction in chat.js replaces direct textarea access across 7 callsites (chat.js, tokens.js, attachments.js).
  • Extension editor in admin-handlers.js uses CM.codeEditor() with JSON and JavaScript modes, replacing bare <textarea> with manual Tab handler.
  • Service worker excludes /vendor/codemirror/ from cache (version-busted).
  • Debug state snapshot includes CM6 version and language list.
  • ARCHITECTURE.md updated to v0.17 reflecting CM6 integration, SQLite dual-driver, modular frontend file structure, and theme system.
  • CI pipeline header updated to v0.17.2 with path gating documentation.
  • build-editor.sh falls back to npm install if package-lock.json is missing (belt-and-suspenders for local dev).

Fixed

  • App initialization crash. Events.publish (nonexistent) → Events.emit (correct API). The unhandled exception during initAppearance() killed initListeners(), leaving the entire app half-initialized — settings modal unclosable, keyboard shortcuts unwired, paste handlers missing.
  • Cursor invisible in dark and light mode. CM6 cursor used --accent color (low contrast on both themes). Switched to --text for consistent visibility. Added explicit borderLeftWidth: 2px.
  • Placeholder text offset. Double padding between .cm-editor wrapper (12px) and .cm-content (8px) pushed placeholder 20px below expected position. Zeroed .cm-content padding — wrapper is the single source of truth.

[0.17.1] — 2026-02-27

Added

  • SQLite backend. Full dual-driver database layer — set DB_DRIVER=sqlite to run with an embedded SQLite database. Pure Go (no CGO), zero external dependencies. 19 store files covering all domain stores: channels, messages, users, teams, personas, knowledge bases, notes, usage, audit, extensions, and more. Feature parity with Postgres including knowledge base vector search via app-level cosine similarity computed in Go.
  • Dialect-aware test infrastructure. database.SetupTestDB() detects DB_DRIVER and provisions either a Postgres test database or a SQLite temp file. Exported database.PH(n) returns $N or ? per dialect. database.TruncateAll() uses TRUNCATE CASCADE on Postgres and DELETE FROM with PRAGMA foreign_keys toggling on SQLite. dialectSQL() helper in handler tests converts $N placeholders and strips ::jsonb casts at runtime.
  • SQLite CI pipeline. New test-sqlite job runs the full handler integration test suite and store tests against an embedded SQLite database. Parallel with the existing Postgres test job. Build gate verifies CGO_ENABLED=0 compilation.
  • Generic provider test config. Live provider integration tests now read PROVIDER, PROVIDER_KEY, and PROVIDER_URL environment variables instead of hardcoded VENICE_API_KEY. Legacy fallback preserved. Model selection prefers non-reasoning models to avoid thinking budget requirements with low max_tokens.

Changed

  • kb_chunks table in SQLite schema includes embedding TEXT column for JSON-encoded float64 vectors (previously omitted as feature-gated).
  • SimilaritySearch on SQLite loads candidate chunks, decodes JSON embeddings, and computes cosine distance in Go — replacing the previous "not available" error.
  • InsertChunks on SQLite now stores embedding vectors as JSON text.
  • Live test names genericized: TestLive_Venice*TestLive_*.
  • CI build-and-deploy depends on [test, test-frontend, test-sqlite].

Fixed

  • SQLite RETURNING + time.Time scan failure. The modernc/sqlite driver cannot scan datetime('now') TEXT columns into time.Time via RETURNING. All 13 SQLite store Create methods rewritten to set timestamps in Go (time.Now().UTC()) and use ExecContext instead of QueryRowContext(...).Scan(). Format: 2006-01-02 15:04:05 (timeFmt constant in helpers.go).
  • SQLite missing id in INSERTs. Unlike Postgres (DEFAULT gen_random_uuid()), SQLite TEXT PRIMARY KEY columns have no auto-generation. Added store.NewID() / uuid.New() to: usage_log, model_pricing (both upsert paths), team_members (AddMember), group_members (AddMember), refresh_tokens (CreateRefreshToken).
  • Test seed helpers SQLite-aware. SeedTestUser, SeedTestChannel, SeedTestTeam, SeedTestTeamMember, SeedTestGroup, SeedGroupMember now branch on IsSQLite() to provide application-generated UUIDs instead of relying on RETURNING id.
  • Postgres-isms in SQLite stores. extension.go Update used now()datetime('now'); ListForUser COALESCE used true1.
  • SQLite store bool/int type mismatches: persona.go auto-fetch flag, usage.go exclude-BYOK filter, user_settings.go visibility map values — all corrected from 0/1 to false/true.

[0.17.0] — 2026-02-27

Added

  • Persona-KB binding. Personas can now have knowledge bases directly bound to them. When a user selects a persona with bound KBs, the kb_search tool is automatically scoped to those KBs and the persona's system prompt includes a KB listing hint. Admin and team admin preset forms include a KB picker with per-KB auto-search toggles. Migration adds persona_knowledge_bases join table.
  • Enterprise KB mode. New discoverable flag on knowledge bases controls whether users can see and attach KBs directly. When kb_direct_access platform policy is set to true, users cannot attach KBs to channels directly — they access KBs exclusively through persona bindings curated by admins. New ListDiscoverableKBs and SetDiscoverable endpoints.
  • Role fallback alerts (issue #69). When a role's primary provider fails and the fallback activates, the resolver emits a role.fallback event on the EventBus with audit log entry. Admin users see a persistent dismissable banner. 5-minute per-role cooldown prevents flooding.
  • Chat rename. Double-click any chat title in the sidebar to edit inline. Enter saves, Escape cancels.
  • Utility model auto-naming. After the first assistant response, a background request to the utility role generates a concise title. Falls back to truncation when no utility model is configured. New endpoint: POST /channels/:id/generate-title.
  • Chat token count. Conversation token estimate shown in the model bar, color-coded against context budget.
  • State restore on refresh. Active chat ID persisted to sessionStorage, auto-restored on page reload.

Fixed

  • ResolvePreset group access bypass. ResolvePreset() used raw SQL that skipped resource_grants checks from v0.16.0. Now uses PersonaStore.UserCanAccess().
  • KB create scope authorization. Now enforces admin role for global KBs and team admin role for team KBs.
  • Completion handler persona ID scoping. personaID variable scoped inside an if block but referenced downstream. Fixed by threading through function signatures.
  • Nil slice JSON marshaling. ListDiscoverableKBs returns [] instead of null when no KBs match.

Changed

  • UpdateDocumentStorageKey moved from raw ExecContext to store method.
  • EventBus route table: role.fallbackDirToClient.
  • Resolver gains .WithBus(bus) builder (nil-safe, backwards compatible).
  • Service worker: /extensions/ excluded from fetch handler.
  • Mermaid renderer v2.0 (pan/zoom, export) promoted to builtin.
  • Removed DIAG diagnostics from TestGroupBasedPersonaAccess.
  • Paste-to-file threshold synced from backend PublicSettings (storage.paste_to_file_chars, admin-configurable, default 2000). Resolves hardcoded constant in attachments.js.
  • Embedding dropdown already had tolerant type filter, manual model ID fallback, and auto-switch on empty — confirmed complete, checkbox updated.

[0.16.0] — 2026-02-27

Added

  • User groups. Global and team-scoped groups decoupled from team membership. groups and group_members tables. Admin and team admin CRUD for group management. Groups serve as ACL targets for resources.
  • Resource grants. Three-way grant model (team_only, global, groups) for Personas and Knowledge Bases. resource_grants table with grant_scope and granted_groups UUID[] columns. Grant picker UI on Persona and KB forms with group multi-select.
  • Schema consolidation. 9 incremental migrations collapsed into single 001_v016_schema.sql. Fresh installs use the consolidated file; upgrade path preserved via migration version tracking.

Changed

  • Persona and KB list queries now filter through resource_grants for non-admin users. Team-only remains the default scope.
  • Admin panel shows group membership counts and grant summaries.

[0.15.1] — 2026-02-26

Added

  • attachment_recall tool. Two operations: list returns filenames and metadata for the current channel's attachments; read extracts and returns content by attachment ID. Channel-scoped access control.
  • conversation_search tool. Full-text search across the current channel's message history using PostgreSQL plainto_tsquery. Returns matching messages with timestamps and role context.
  • Token estimator attachment awareness. Tokens.estimateAttachments() accounts for staged file sizes in context budget calculations. Warning thresholds include attachment estimates.

[0.15.0] — 2026-02-26

Added

  • Background compaction scanner. Periodic scan identifies channels exceeding configurable context thresholds. Automatic summarization via utility role compresses old messages into summary nodes. Per-channel opt-in/out via auto_compact channel setting.
  • Compaction service. compaction.Service orchestrates summary generation: estimates token usage, selects messages for compression, calls utility role, inserts summary as tree boundary node with metadata.
  • Context budget guard rail. 80% ceiling prevents compaction from triggering mid-generation. Cooldown timer prevents repeated compaction of the same channel.
  • Summarize & Continue button. User-triggered compaction from the context warning bar. Reuses compaction service with immediate execution.

Changed

  • Scanner configurable via global_settings: threshold percentage, cooldown duration, enabled/disabled toggle. Channel-level overrides.

[0.14.0] — 2026-02-26

Added

  • Knowledge bases. RAG pipeline: upload documents → chunk (recursive text splitter) → embed via pgvector → kb_search tool for semantic retrieval. Team and personal KB scopes. Channel KB toggle enables per-conversation knowledge access.
  • Document ingestion. knowledge.Ingest() pipeline: file upload → text extraction (reuses v0.12.0 pipeline) → chunking with configurable overlap → embedding via the embedding role → storage as kb_chunks with vector index.
  • KB admin panel. Knowledge Bases section under AI category. Create, delete, upload documents, view chunk counts and storage usage. Team admin scoped to team KBs.
  • Notes semantic search. Note search upgraded from exact text match to pgvector cosine similarity when embeddings are available.
  • kb_search tool. Registered when embedding role is configured. Accepts query text, returns top-K chunks with source document attribution. Channel KB bindings control which KBs are searched.

Changed

  • pgvector extension enabled in schema (CREATE EXTENSION IF NOT EXISTS vector). kb_chunks table includes embedding vector(1536) column with IVFFlat index.

[0.13.1] — 2026-02-26

Added

  • web_search tool. Search provider abstraction with two backends: DuckDuckGo (HTML scraping, zero config) and SearXNG (self-hosted, JSON API). Returns title, URL, and snippet for each result. Configured via SEARCH_PROVIDER and SEARXNG_URL env vars.
  • url_fetch tool. Fetches and extracts text content from URLs. Respects robots.txt. Content-type detection with HTML-to-text conversion. Configurable timeout and size limits.
  • Tool categories. Tools now have a category field (builtin, search, knowledge, browser). Tools toggle UI in chat bar groups by category with per-tool enable/disable.
  • Tools toggle UI. Popup menu on chat input toolbar showing all available tools. Per-tool checkbox state sent as disabled_tools[] in completion requests. Browser extension tools included.

[0.13.0] — 2026-02-25

Changed

  • Admin panel refactor. Replaced 12-tab modal with fullscreen admin panel. Four categories (People, AI, System, Monitoring) with section sidebar navigation. URL-based routing (#admin/people/users). Responsive layout, classification banner-aware positioning.
  • CSS design token cleanup. Consolidated duplicate color/spacing variables. Admin panel uses shared token system with main UI.

[0.12.0] — 2026-02-25

Added

  • File handling and vision support. Upload images, PDFs, and documents into chat via 📎 button, drag-and-drop, or paste. Multimodal message assembly injects base64 images for vision-capable models and extracted text for documents. Staged attachment strip shows upload progress and extraction status. Auth-aware blob rendering with Bearer tokens. Image lightbox viewer. Vision capability hints on image attachments.
  • Storage backend abstraction. ObjectStore interface with two implementations: PVC (local filesystem) for single-node/dev and S3 (minio-go) for multi-node production. S3 backend works with any S3-compatible API: MinIO, Ceph RGW, AWS S3. Auto-detection when STORAGE_BACKEND is not set (tries PVC, disables if not writable). Both backends share identical semantics — all handlers, attachment CRUD, multimodal assembly, and orphan cleanup work regardless of backend.
  • S3 configuration. S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, S3_REGION, S3_PREFIX, S3_FORCE_PATH_STYLE env vars. Endpoint auto-detects SSL from scheme. Path-style URLs default on (required for MinIO/Ceph). Optional key prefix for shared buckets. CI pipeline syncs S3 secrets to K8s when STORAGE_BACKEND=s3.
  • Text extraction pipeline. PDF, DOCX, XLSX, PPTX, ODT, RTF text extraction via filesystem-based queue. Inline and sidecar modes. Crash recovery for items stuck in processing state.
  • Admin storage panel. Backend health, file count, total size, orphan detection and cleanup. Shows PVC path or S3 endpoint/bucket depending on active backend. Extraction queue status.
  • Vault CLI commands. switchboard vault rekey re-encrypts all provider API keys when rotating the encryption key. switchboard vault status shows encryption health. Admin UI encryption status indicator in Settings tab.
  • Per-chat model persistence. Server-side channels.settings JSONB field stores last_selector_id. Roams across devices with localStorage write-through cache as fallback.

[0.11.0] — 2026-02-25

Added

  • Browser extension system. Full lifecycle: manifest registration, script injection, scoped context (ctx.renderers, ctx.tools, ctx.events, ctx.storage, ctx.ui), permission-aware API proxying. Extensions self- register via Extensions.register() and receive isolated contexts during initAll(). Admin CRUD endpoints, asset serving (public, no auth needed for <script> tag loading), auto-seeder for builtin extensions.
  • Custom renderer pipeline. Block renderers intercept code fences by language tag, post renderers process the DOM after insertion. Case- insensitive language matching handles LLM capitalization (Mermaid, Diff, CSV). Nested ```markdown ``` fence unwrapping prevents the common LLM pattern of wrapping responses in markdown fences from breaking inner code blocks.
  • Browser tool bridge. Extensions register tool handlers via ctx.tools.handle(). Server collects browser tool schemas alongside server tools, includes them in LLM completions. Tool calls route through WebSocket EventBus (tool.call.* → browser → tool.result.* → server). 30-second timeout with error recovery.
  • Server tools: calculator and datetime. Auto-register via init(), zero wiring changes. Calculator: recursive-descent evaluator with arithmetic, 17 math functions, constants. Datetime: current date/time/ timezone/unix/ISO-week in any IANA timezone.
  • 6 built-in browser extensions (self-contained, own CSS via _injectStyles()/destroy() lifecycle):
    • Mermaid: block + post renderer, SVG diagrams, dark mode detection, local vendor with CDN fallback, max-height constraint with scroll
    • KaTeX: block renderer (```latex/math/tex ```) + post renderer (inline$...and...$` in text nodes)
    • CSV Table: block renderer, RFC 4180 parser with quoted fields, sortable columns (click headers), numeric-aware sorting
    • Diff Viewer: block renderer, red/green syntax highlighting for unified diffs, stats badge (+N/-N), hunk/file headers
    • JS Sandbox: browser tool (js_eval), sandboxed iframe (allow-scripts only), console capture, 10s timeout
    • Regex Tester: browser tool (regex_test), multi-input matching, full match details with named groups and indices
  • Admin extension editor. Edit button on each extension in Admin → Extensions. Inline editor shows name, description, manifest JSON, and script source with tab-key support. System extensions show overwrite warning.
  • Enhanced diagnostics. Test 5: browser extensions (loaded, active, renderers, tool handlers). Test 6: Service Worker cache (registration, scope, state, cache names, entry counts). Purge Cache button in Debug Log modal footer.
  • Extension-owned styles. All 4 rendering extensions (mermaid, katex, csv, diff) inject their own <style> tags during init() and remove them during destroy(). Global stylesheet only keeps .ext-rendered wrapper. User-created extensions can bring their own CSS without touching the global stylesheet.
  • Notes extension rendering. runExtensionPostRender() called in both note read mode and preview mode. Mermaid diagrams and KaTeX math now render correctly in notes.
  • Database migration 006_extensions.sql. extensions and extension_user_settings tables.

Fixed

  • WebSocket token field mismatch. API saved { accessToken: '...' } but EventBus read tokens.access. Tool bridge was dead on arrival.
  • Extension asset 401. Asset route was behind auth middleware, but <script> tags don't send Authorization headers. Moved to public group.
  • runExtensionPostRender is not defined. Stale Service Worker cache served old ui-format.js without the function. Added typeof guard.
  • Extension init order. Extensions loaded after loadChats() — block renderers not registered when messages first rendered. Moved extension loading before chat loading.
  • K8s Ingress WebSocket routing. Traefik pathType: Exact doesn't reliably win over Prefix rules in the same Ingress resource. Changed /ws and /health to Prefix for longest-prefix-wins routing.
  • Mermaid SVG oversized. Added max-height: 600px on diagram container with overflow scroll, removed mermaid.js hardcoded height attribute from SVGs so CSS constraints apply.

Changed

  • Extension CSS removed from global styles.css. Each extension owns its styles via _injectStyles() in init() with destroy() cleanup. Idempotent injection guards prevent duplicates.
  • Markdown fence language extraction lowercased at the point of extraction in ui-format.js, making all block renderer pattern matches case-insensitive without per-extension workarounds.
  • _unwrapMarkdownFence() pre-processor strips outer ```markdown ``` wrappers when they contain nested fences. Handles think-block placeholders and trailing explanation text. Only triggers when nested fences are present — plain markdown code blocks still render normally.

[0.10.5] — 2026-02-24

Added

  • ui-primitives.js — shared rendering primitives and registries. Single source of truth for provider types, role definitions, and reusable UI components. Extension-ready via registry pattern (Providers.add(), Roles.add()). Primitives follow the renderPresetForm() pattern: (container, options) → control object with getValues/setValues/clear.
    • Providers registry — types, labels, default endpoints (was 5 places → 1)
    • Roles registry — names, type filters, hints (was hardcoded in 2 places → 1)
    • renderCapBadges(caps, opts) — consolidates 3 badge builders (compact + detailed)
    • renderProviderForm(container, opts) — replaces 3 form implementations
    • renderProviderList(container, opts) — replaces 3 list renderers
    • renderRoleConfig(container, opts) — replaces 2 role UIs + 4 handlers
    • renderUsageDashboard(container, opts) — replaces 3 usage renderers

Fixed

  • Admin provider form now auto-fills endpoint on type change. Was missing from admin (worked in user BYOK and team forms). Now all three scopes use the same primitive with identical behavior.
  • Team provider form consolidated. Was two separate HTML forms (create + edit) with separate listeners. Now a single dual-mode form matching the pattern used by admin and user BYOK scopes.

Changed

  • Provider type definitions removed from index.html (2 static <select>s) and settings-handlers.js (1 dynamic build + 2 endpoint maps). All now sourced from Providers registry in ui-primitives.js.
  • Provider list rendering uses event delegation instead of inline onclick handlers. Each list returns { refresh, getCache } control handles.
  • Role configuration uses data-role-* attributes for event delegation instead of id-based selectors and global onchange handlers.
  • Usage dashboards accept options for compact/full mode, custom API calls, and extension-provided extra columns.
  • All 16 confirm() calls replaced with showConfirm() — styled modal dialog matching the app design instead of browser-native dialog. Supports danger styling, Escape/Enter keys, click-outside dismiss.
  • Sidebar collapse icon now always visible (dimmed) next to the logo, brightens on hover. Previously the icon replaced the logo on hover only. When sidebar is collapsed, only the collapse icon shows (logo hidden).
  • New .popup-menu + .popup-menu-item shared CSS base for all dropdown and flyout menus. createPopupMenu(anchor, opts) primitive available for future menu creation with consistent behavior.
  • Removed ~360 lines of duplicated code across admin-handlers.js, settings-handlers.js, ui-admin.js, and ui-settings.js.
  • Removed ~40 lines of static HTML form markup from index.html.

[0.10.4] — 2026-02-24

Added

  • model_type field across the full pipeline. Models now carry a type classification (chat, embedding, image) sourced from provider APIs at sync time — no hardcoded lists. Venice's /v1/models returns type per model; OpenAI-compatible APIs pass through the field when present.
    • New DB column: model_catalog.model_type VARCHAR(20) DEFAULT 'chat'
    • Migration: 005_model_type.sql
    • Propagation: providers.Model.TypeCatalogSyncEntry.ModelTypeCatalogEntry.ModelTypeUserModel.ModelType → frontend model_type

Fixed

  • Admin role save didn't refresh UI. adminSaveRole() showed "✓ Saved" but never called UI.loadAdminRoles(), so dropdowns appeared stale after save. Now reloads the roles panel after a successful save.
  • Role model dropdowns showed all models regardless of type. Embedding role showed chat models, utility role showed embedding models. Both admin and user role UIs now filter the model dropdown by model_type:
    • "embedding" role → only model_type === 'embedding' models
    • "utility" role → only model_type === 'chat' models

Changed

  • Venice provider now reads the type field from each model in the API response and normalizes it (textchat, embeddingembedding, imageimage).
  • OpenAI provider wire type extended with optional type field for OpenAI-compatible APIs that include it.

[0.10.3] — 2026-02-24

Changed

  • Frontend refactor: 2 monolith files → 13 domain-scoped files. Split ui.js (2,582 lines) and app.js (2,940 lines) into 13 focused files averaging ~544 lines each. No features added, no functions renamed, no architectural changes. Vanilla JS, no modules, no build step.

    New file structure:

    File Lines Domain
    ui-format.js 353 Markdown rendering, esc(), code blocks, side panel
    ui-core.js 974 UI object: sidebar, chat list, messages, streaming, model selector
    ui-settings.js 640 Settings tabs, teams, providers, user preferences
    ui-admin.js 645 Admin tabs, users, roles, usage, teams
    tokens.js 123 Context tracking, token estimation
    notes.js 364 Notes panel, editor, multi-select
    chat.js 584 Chat ops, send, regen, edit, branch, summarize
    settings-handlers.js 692 Settings save, provider CRUD, command palette
    admin-handlers.js 652 Admin actions, presets, team management
    app.js 567 State, init, boot, auth, listener dispatch

    Unchanged: api.js (575), debug.js (580), events.js (327).

  • initListeners() decomposed into domain-specific init functions: _initChatListeners(), _initSettingsListeners(), _initAdminListeners(), _initNotesListeners(), _initGlobalKeyboard(). The orchestrator in app.js dispatches to each.

  • Side panel resize changed from self-invoking IIFE to _initSidePanelResize() called during listener init, avoiding DOM timing issues.

  • Service worker updated with new file list for pre-caching.

  • Policy-gating tests updated to read from the correct source files after the split. All 159 tests pass.

[0.10.2] — 2026-02-24

Added

  • Summarize & Continue — User-triggered conversation compaction using the utility model role. Button appears in context warning bar at ≥75% context usage. POST /channels/:id/summarize calls the utility role to generate a summary, inserts it as a tree node with metadata.type = "summary", and updates the cursor. Subsequent completions use the summary as a context boundary — messages before it are replaced by the summary as a system message. Multiple summaries stack. Fork-aware (summaries are tree nodes with their own branch position). "Show full history" toggle reveals collapsed earlier messages.
  • BYOK Role Overrides — Users with personal providers can override the org's utility and embedding model roles. Resolution chain: personal → team → global. New "Model Roles" tab in Settings (visible when BYOK is enabled). Stored in user.settings JSONB under model_roles key, same shape as team overrides.
  • Utility Rate Limitingutility_rate_limit global setting (default: 20 calls/hour/user, 0 = unlimited). Org-funded utility calls check against usage_log before executing. BYOK calls exempt (user pays their own way). Returns 429 with clear message when exceeded.
  • Message metadata in pathPathMessage now includes metadata JSONB field, enabling summary detection and future message-type extensibility.
  • Per-chat model/preset restore — Switching between chats now restores the last-used model or preset in the selector. Stored in localStorage keyed by channel ID. Falls back to the channel's base model, then the global default, if the original selection is no longer available. Cleaned up on chat deletion.

Changed

  • Generation role removedRoleGeneration ("generation") removed from ValidRoles and admin Roles tab. Image/media generation will be extension-managed (v0.11.0) with its own provider config, not a role slot. The current completion/embedding abstraction doesn't fit image gen's fundamentally different API surface.
  • Resolver.Complete/Embed signatures — Now accept userID parameter for personal role override resolution. Empty string skips personal lookup.
  • Proactive token refresh — Access token is now refreshed at 80% of its lifetime (~12min for 15min tokens). On page reload, a conservative 60s refresh is scheduled since token age is unknown. Eliminates the race condition where profile succeeds on a near-expired token but subsequent calls fail.
  • Auth guard in startApp() — If token is invalidated during startup (e.g., 401 on loadChats after profile succeeded), app returns to login instead of rendering a broken UI with cascading 401 errors.
  • Per-chat model restore fallback — When the stored model/preset is removed, falls back to admin default → first visible model (was keeping stale selection). Also cleans up the stale localStorage entry.
  • BYOK Role Overrides — Fixed response parsing (configs.configs not configs.data) and model field mapping (configId/baseModelId not provider_config_id/model_id).
  • GetRole endpoint integration testGET /admin/roles/:role now exercised by CI via TestIntegration_Roles_GetSingleRole, catching the GetConfig signature mismatch that caused the build failure.
  • Auth resilience frontend tests — 10 tests covering startup auth guard, token refresh lifecycle, and the profile-success-then-401 edge case.
  • JS syntax lint in CInode --check runs on all src/js/*.js files before frontend tests, catching parse errors that tests alone can't detect.

[0.10.1] — 2026-02-24

Added

  • Admin System Prompt — Global system prompt configured in Admin Settings. Injected as the first system message in every conversation, before user/preset system prompts. Users cannot override or disable it. Stored in global_settings['system_prompt'].
  • Personas tab — User presets moved from the Models tab to their own "Personas" tab in Settings. Tab is hidden when admin disables user presets (allow_user_personas policy).
  • Team Management modal — Team admin functionality extracted from Settings into its own tabbed modal (Members, Providers, Presets, Usage, Activity). Accessed via "Team Management" flyout menu item, or "Manage →" on team cards in Settings. Multi-team admins see a picker first; single-team admins go straight to the tabbed view. Back arrow in header returns to picker.
  • Preview pane: clear button — Trash icon in the preview header resets the iframe and shows the empty hint. Also auto-clears when deleting a chat that had active preview content.
  • Preview/Notes pane: fullscreen — Expand button in the header toggles fullscreen mode (panel fills entire viewport width).
  • Preview/Notes pane: resizable — Drag the left edge of the side panel to resize (280px70vw). Width resets on close.
  • Code block: download — "Download" button on every code block. Infers file extension from language tag (e.g. python.py, go.go).

Changed

  • Personal Usage scoped to BYOKGET /usage and the user Usage tab now only show consumption against personal (BYOK) providers. Global provider usage is the org's cost, not the user's. New QueryByUserPersonal store method filters on scope = 'personal' AND owner_id = user_id. Admin usage views remain unaffected. Usage tab hidden when admin disables user providers (allow_user_byok policy).
  • OpenAI streaming usage race — Deferred the Done event until the usage chunk arrives. Previously, finish_reason fired Done before the usage chunk (which has choices: []), so streaming token counts were always 0 for all OpenAI-compatible providers.

Fixed

  • Usage logging zero-token guard — Removed early exit in logUsage that suppressed rows when tokens were 0. Combined with the streaming race above, this meant no streaming usage was ever recorded.
  • Live chat completion testTestLive_VeniceChatCompletion sent wrong field names (messages/config_id instead of content/provider_config_id).

[0.10.0] — 2026-02-24

Added

  • Model Roles — Named model slots (utility, embedding, generation) with primary + fallback bindings. Stored in global_settings['model_roles']. Team-level overrides via teams.settings JSONB. New server/roles/ package with Resolver.Complete() and Resolver.Embed() for automatic failover. Admin API: GET/PUT /admin/roles/:role, POST /admin/roles/:role/test. Team API: GET/PUT/DELETE /teams/:id/roles/:role.
  • Provider Embed() interface — All providers implement Embed(). OpenAI, Venice, OpenRouter support /v1/embeddings; Anthropic returns ErrNotSupported. New types: EmbeddingRequest, EmbeddingResponse.
  • Usage Tracking — Every completion (streaming and non-streaming) logs token counts and cost to usage_log table. Cost calculated at insert time from model_pricing (no retroactive recalculation). Provider scope denormalized for efficient admin filtering. Admin views exclude BYOK. New stores: UsageStore, PricingStore. Admin API: GET /admin/usage, GET /admin/usage/users/:id, GET /admin/usage/teams/:id. User API: GET /usage (personal summary).
  • Model Pricingmodel_pricing table with catalog sync and manual admin overrides. Sync from provider APIs (Venice, OpenRouter) auto-populates pricing with source='catalog'; manual entries never overwritten. Admin API: GET/PUT/DELETE /admin/pricing.
  • Streaming token capture — OpenAI: stream_options.include_usage + parse usage/cache from final chunk. Anthropic: parse message_start for input/cache tokens, message_delta for output tokens. Cache token fields (CacheCreationTokens, CacheReadTokens) on CompletionResponse, StreamEvent, and streamResult.
  • Admin Roles tab — Configure primary/fallback provider+model per role, test-fire from UI.
  • Admin Usage dashboard — Period selector (7d/30d/90d), group-by (model/user/day/provider), totals cards, breakdown table, pricing table.
  • Team Usage dashboard — Team admins can view usage against their team-owned providers via GET /teams/:teamId/usage. Filters to provider_configs WHERE scope='team' AND owner_id=teamId. Integrated into team management panel with period/group-by selectors.
  • Admin Reset Password — Button in user list with vault destruction warning dialog (two-step confirmation).
  • User Usage tab — Settings modal "Usage" tab shows personal token consumption and estimated costs across all conversations, including BYOK. Period selector (7d/30d/90d) and group-by (model/day).
  • Live Venice integration tests — Gated behind VENICE_API_KEY env var. Tests: non-streaming completion, streaming completion, usage logging for both modes, pricing from catalog sync, and direct embeddings via BGE-M3. Uses qwen3-4b (Venice Small) — cheapest at $0.05/$0.15 per 1M tokens.
  • Migration 004usage_log table, model_pricing table, model_roles seed in global_settings.

Fixed

  • UEK re-wrap on password changeChangePassword now decrypts UEK with old password and re-encrypts with new password + fresh salt. Previously, password changes silently broke all personal BYOK keys.
  • UEK destruction on admin password resetResetPassword now nullifies vault columns, evicts UEK from cache, deletes personal provider configs, and logs an audit event. Previously, admin resets left orphaned encrypted UEK that silently broke personal keys.
  • Streaming completions logged zero tokensStreamEvent lacked token fields and providers discarded usage data from final chunks. Both OpenAI and Anthropic streaming parsers now capture and propagate token counts.
  • OpenAI streaming usage race condition — The OpenAI protocol sends chunks in order: content → finish_reason → usage → [DONE]. The parser sent Done=true on the finish_reason chunk (step 2) before the usage chunk arrived (step 3, with choices:[]). The receiver returned immediately on Done, so usage was captured but never delivered. Fix: defer the Done event as pendingFinish until the usage chunk arrives, then flush with tokens attached. Tool-call finishes flush immediately (tool loop needs the event). Affects all OpenAI-compatible providers (OpenAI, Venice, OpenRouter).
  • Token accumulation across tool iterationsstreamResult in stream_loop.go now accumulates input/output/cache tokens across multi-tool-call iterations instead of only capturing the final iteration.
  • Admin pricing leaked BYOK entriesPricingStore.List() returned all model_pricing rows including those from personal BYOK providers. Admin panel showed pricing entries for user-private providers they shouldn't see, with O(users × models) scale explosion. Now joins on provider_configs and filters scope != 'personal'. Admin UpsertPricing also validates provider scope, rejecting manual pricing on personal providers.
  • Team role handlers used wrong param nameListTeamRoles, UpdateTeamRole, DeleteTeamRole read c.Param("id") but routes use :teamId. Every team role operation silently got an empty team ID.
  • Usage logging suppressed for zero-token streamslogUsage had an early exit when inputTokens == 0 && outputTokens == 0. Combined with the OpenAI streaming parser bug (below), this silently dropped all streaming usage rows. Removed the guard — requests are always recorded.
  • Live chat completion test used wrong field namesTestLive_VeniceChatCompletion sent messages and config_id but the handler expects content and provider_config_id. Test silently skipped on the binding error.

[0.9.4] — 2026-02-24

Added

  • API key encryption (vault) — Two-tier AES-256-GCM encryption for stored API keys. Global/team keys encrypted with env-var-derived key (HKDF-SHA256); personal BYOK keys encrypted with per-user encryption key (UEK) derived from password via Argon2id. Admin cannot recover personal keys without user's passphrase. New server/crypto/ package: vault.go, cache.go, resolver.go, backfill.go with 11 round-trip tests.
  • UEK lifecycle — UEK generated on registration, unwrapped on login (Argon2id → AES-GCM), cached in sync.Map for session duration, evicted with memory zeroing on logout. Pre-migration users auto-initialize vault on first login.
  • Migration 003_vault.sql — Adds encrypted_uek, uek_salt, uek_nonce, vault_set to users table. Adds api_key_enc (BYTEA), key_nonce, key_scope to provider_configs. Backfills key_scope from existing scope.
  • Startup backfillBackfillEncryptedKeys() encrypts plaintext API keys on first startup with ENCRYPTION_KEY set. EnforceEncryptionKey() refuses startup if encrypted keys exist but env var is missing.
  • CI/CD: encryption secretENCRYPTION_KEY Gitea secret synced to k8s switchboard-encryption secret. Backend manifest references it as optional secretKeyRef.

Fixed

  • HTML code blocks render live in chat (XSS) — When a model's </think> tag directly abutted a code fence (no newline), the fence wasn't recognized by marked.js, causing raw HTML to render as live DOM elements (canvas games, styles, etc.). Three-layer fix: (1) DOMPurify switched from permissive ADD_TAGS (default allows canvas, style, form, etc.) to strict ALLOWED_TAGS allowlist of only markdown-produced elements; (2) think-block placeholders padded with \n\n to ensure adjacent fences start on fresh lines; (3) unclosed code fences auto-closed before marked.parse for streaming protection.

[0.9.3] — 2026-02-23

Changed

  • Code blocks: button-driven collapse — Replaced <details> wrapper with inline collapse/expand toggle button in the code toolbar. Auto-collapses at

    15 lines with a fade mask; toggle available on all blocks >5 lines. User can always expand/collapse regardless of threshold. Smooth CSS transition instead of native <details> jump.

  • Thinking blocks: always visible — Thinking blocks are always rendered in the DOM regardless of the showThinking setting. The setting now controls whether blocks start expanded (<details open>) or collapsed. User can always click to toggle. Setting label updated to "Auto-expand thinking blocks".

Added

  • Admin default model — New default_model policy in Admin → Settings. Dropdown populated from enabled models. When a user has no saved selection (fresh login, cleared browser) or their saved model is no longer available, the admin default is used before falling back to first visible. Resolution chain: localStorage → admin default → first visible.
  • Reasoning/thinking support for OpenAI-compatible providers — Models that send reasoning_content in stream deltas (Grok, DeepSeek, etc.) now have thinking blocks streamed live and persisted as <think> tags. Renders as collapsible thinking blocks in both streaming and history views.
  • Favicon animation during generation — Browser tab favicon pulses with cascading dot opacity animation while a completion is in progress, restoring to the static favicon when done.
  • Tool calls in message history — Tool call metadata (name, arguments, result, error status) is now persisted alongside assistant messages in the database. History view renders tool calls as collapsed <details> blocks showing "🔧 tool_name → done" with expandable input/output JSON.
  • Notes tool: "View note" link — When a notes tool (note_create, note_update, etc.) completes, a "📝 View note" button appears in both the live streaming tool indicator and the history tool call block. Clicking opens the Notes panel and navigates directly to the note.
  • Notes panel: Copy button — "Copy" button added to note read mode, copies title + content as markdown to clipboard.
  • Brand hover: jitter-free crossfade — Sidebar brand logo→collapse icon swap uses opacity crossfade instead of display: none toggle, eliminating layout reflow jitter on hover.

Fixed

  • Model selector shows unavailable model — Selector restoration searched all models including user-hidden ones. Saved selection (localStorage) for a hidden model like Claude Opus would re-select it on every page load even when only Grok was visible. Resolution now filters to visible models only.
  • Refresh toast shows wrong count — "Loaded 33 models" counted all models including hidden; now shows only visible count ("Loaded 1 model").
  • Tool calls vanish after completion — Live streaming tool indicators disappeared when reloadActivePath() rebuilt messages from DB. Fixed: the getActivePath query now includes tool_calls column, and PathMessage carries the data through to the frontend renderer.
  • Tool result "undefined results" text — Operator precedence bug in tool result summary parser caused undefined results to display for note_create. Fixed summary logic to properly branch on title vs count.
  • Regenerate streams at wrong position — Regen'd response appeared below the full conversation (appended at bottom) then snapped to correct position after completion. Fixed: display is now truncated to the parent message before streaming starts, so the new response streams in-place as a clean branch. Backend context was already correct (excludes old response).
  • Regenerate loses tools, reasoning, and tool execution — Regen handler was a stripped-down copy of the completion handler missing tool definitions, tool execution loop, reasoning_content forwarding, and tool_calls persistence. Model lost access to note tools on regen and fell back to generic capabilities. Refactored: extracted streamWithToolLoop() into stream_loop.go as the single canonical streaming implementation. Both streamCompletion (normal chat) and Regenerate now call the shared function — only persistence differs. Future streaming features (new event types, tool capabilities) automatically apply to all code paths.
  • OpenRouter free models crash — Free models with nil pricing caused pq: invalid input syntax for type json on catalog sync. Nil pricing now routes through ToJSON() producing "{}" instead of nil []byte.
  • API key appears unsavedListGlobalConfigs returned raw ProviderConfig structs where APIKeyEnc is json:"-" (never serialized), so has_key was always undefined. Now returns computed has_key field.
  • Case-insensitive usernames — Login, registration, and all user lookups use LOWER() in SQL. All user creation paths normalize to lowercase. New migration 002_ci_username.sql adds LOWER() unique indexes and normalizes existing rows.

[0.9.2] — 2026-02-23

Added

  • Collapsible code blocks: Code blocks over 15 lines auto-collapse into <details> with language and line count summary. Language label shown in top-left corner of all code blocks.
  • HTML preview: "Preview" button on HTML code blocks opens a sandboxed iframe (allow-scripts, no allow-same-origin). Auto-detected for untagged blocks via heuristic.
  • Token count estimate: Live token counter below input area showing approximate tokens and context usage percentage when model has max_context.
  • Context length warning: Dismissable banner above input at 75% (yellow) and 90% (red) context usage with guidance to start a new chat.
  • Proxy interception detection: _parseJSON() checks Content-Type before .json() on all API paths including streaming. Typed proxyBlocked errors with proxy page title extraction and actionable splash messages.
  • Environment injection: window.__ENV__ wired through entrypoint, k8s, and index.html for dev/test/production gating.
  • Enhanced diagnostics: NET:PROXY log type, Content-Type capture in fetch interceptor, environment info in export header and state snapshot.
  • Team admin audit scoping: New GET /api/v1/teams/:teamId/audit and /audit/actions endpoints scoped to team members. Activity Log section in team manage panel with filter dropdown and pagination.
  • New favicon: Switchboard panel design with provider-colored jacks. Animated SVG (rotating plugs), 32px PNG, 256px PNG, and ICO.
  • Seed users (dev/test only): SEED_USERS=user:pass:role,... env var pre-creates active users on startup. Ignored in production. K8s secret switchboard-seed-users wired in backend manifest.

[0.9.1] — 2026-02-23

Removed

  • Static known model table: Deleted knownModels map from backend and KNOWN_MODELS from frontend. The same model ID can have different capabilities depending on the provider (e.g. DeepSeek has tool_calling on OpenRouter but not on Venice). A hardcoded table can't represent this.
  • Frontend lookupKnownCaps(): Removed client-side capability guessing. Backend is the sole source of truth via catalog → heuristic chain.

Changed

  • Resolution chain simplified: catalog (provider API sync) → heuristic inference. No intermediate known table. Providers that report capabilities via API are authoritative; heuristics are best-effort for unsynced models.
  • All providers updated: OpenAI, OpenRouter, Anthropic, Venice now call InferCapabilities() directly instead of the dead known table lookup.
  • Frontend resolveCapabilities(): Now passes through backend caps as-is. No client-side merging with a static table.
  • EXTENSIONS.md recovered into repo, updated with Appendix A (Custom Renderers) and Appendix B (Model Roles with utility/embedding/generation slots)
  • ROADMAP.md restructured: extension foundation pulled to v0.11.0, model roles to v0.10.0, dependency graph, TBD replaces post-1.0, removed v0.8→v0.9 migration (OBE — no public release, no test path)

Added

  • Heuristic patterns: Updated to detect qwen3, gpt-5, grok, kimi, minimax, glm-5, gemma-3 model families. Vision expanded to claude-opus/sonnet (not just claude-3). Reasoning expanded for thinking, grok, glm patterns.

Fixed

  • Preset capability pills: Presets with auto-resolve (no provider_config_id) now inherit base model capabilities via GetByModelIDAny catalog fallback.
  • Venice optimizedForCode: Added mapping to CodeOptimized capability.
  • CI test stability: BYOK journey tests use unreachable endpoints so auto-fetch doesn't race with simulated data injection.

[0.9.0] — 2026-02-22

Added

  • Schema consolidation: 21 migrations collapsed to single 001_initial.sql
  • Store layer: All database access through typed interfaces (no raw SQL in handlers)
  • Persona model: Trust-boundary model replacing old presets; scoped global/team/personal
  • Capabilities resolver: Three-tier chain — catalog → known table → heuristic inference
  • Three-state model visibility: enabled / disabled / team-only
  • BYOK auto-fetch: Creating a personal provider triggers model discovery from provider API
  • User model refresh: POST /api-configs/:id/models/fetch endpoint + UI button
  • Composite model IDs: configId:modelId format prevents cross-provider collisions
  • Audit log foundation: All admin operations logged with actor, action, resource
  • Journey integration tests: API-driven test suite replacing fake-data tests
  • Frontend test suite: 107 tests, 27 suites validating model processing pipeline
  • Live Venice API test: Proves real BYOK → auto-fetch → models visible flow

Fixed

  • API key storage: json:"-" tag on ProviderConfig.APIKeyEnc silently dropped keys during admin create/update. Fixed with wrapper structs that bypass the tag.
  • NULL model_default scan: scanProviders() crashed on NULL model_default column, silently hiding all team and BYOK models. Fixed with sql.NullString.
  • Nil slice serialization: Go nil slices serialized as JSON null instead of [], breaking frontend fallback chains. Fixed with make([]T, 0).
  • Frontend error swallowing: API responses with errors field were silently ignored.

Changed

  • Backward-compatible API routes with v0.8 field name aliases
  • User model preferences table (user_model_settings)