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/docs/archive/DESIGN-0.27.0.md
2026-03-11 14:45:37 +00:00

30 KiB
Raw Blame History

DESIGN — v0.27.0+: Debt Clearance, Extension Surfaces, Tasks

Status: Draft Scope: Deferred debt clearance (v0.27.0), Tasks / Autonomous Agents (v0.27.x), TBD pull-forward (v0.28.0) Depends on: Workflow Engine (v0.26.0), Dynamic Surfaces (v0.25.0), Permissions (v0.24.2) Current version: 0.26.5


Versioning Plan

v0.27.0  Debt Clearance: Extension Surface Routes + Workflow Polish
           │
v0.27.1  Tasks Foundation: Service Channels + Scheduler
           │
v0.27.2  Task Execution: Budgets + Admin Controls
           │
v0.27.3  Task Chaining: Webhooks + Workflow-to-Workflow
           │
v0.27.4  Personal Tasks: BYOK Scheduling + User Task UI
           │
v0.28.0  Platform Polish (TBD pull-forward)

Code Audit — Items Already Shipped

The ROADMAP lists these as deferred but the code proves they're done:

Item Evidence Listed as deferred in
Persona tool grant enforcement in completion handler completion.go:928949 — second-pass allowlist via GetToolGrants() v0.25.0, v0.26.0
Version snapshot includes persona tool grants workflows.go:263273stageSnapshot.ToolGrants populated at publish v0.25.0
Session cleanup job Shipped in v0.26.0 (background goroutine, SESSION_EXPIRY_DAYS) v0.24.3

Action: Cross off all three in the ROADMAP's deferred sections and in the v0.25.0 / v0.26.0 milestone checklists.


v0.27.0 — Debt Clearance

All deferred items from v0.25.0 and v0.26.0 plus outstanding tech debt that blocks future work. No new features — just finishing what's owed.

Phase 1: Extension Surface Routes (/s/:slug)

The longest-deferred item (originally v0.21.3). Infrastructure exists: surface_registry table, InstallSurface handler (zip upload + asset extraction), ListEnabledSurfaces API. What's missing is the runtime route mounting, template rendering, and frontend nav integration.

Problem: Hardcoded surface conditionals in base.html

Lines 6166 and 118122 of base.html use {{if eq .Surface "chat"}} chains. Extension surfaces fall through to "Unknown surface". Same pattern for CSS includes (line 2526) and script includes (118122).

Solution: Generic extension surface template + dynamic blocks

base.html changes:
  {{if eq .Surface "chat"}}...
  {{else if eq .Surface "admin"}}...
  ...
  {{else if .Manifest}}{{template "surface-extension" .}}
  {{else}}<div>Unknown surface: {{.Surface}}</div>
  {{end}}

New template templates/surfaces/extension.html:

{{define "surface-extension"}}
<div id="extension-surface" class="extension-surface"
     data-surface-id="{{.Surface}}"
     data-manifest='{{.Manifest | toJSON}}'>
  {{/* Extension JS mounts into this container */}}
  <div id="extension-mount"></div>
</div>
{{end}}

{{define "css-extension"}}
{{/* Extension CSS loaded dynamically from surfacesDir */}}
{{if .Manifest}}
<link rel="stylesheet" href="{{.BasePath}}/surfaces/{{.Surface}}/css/main.css?v={{.Version}}">
{{end}}
{{end}}

{{define "scripts-extension"}}
{{if .Manifest}}
<script nonce="{{.CSPNonce}}" src="{{.BasePath}}/surfaces/{{.Surface}}/js/main.js?v={{.Version}}"></script>
{{end}}
{{end}}

Dynamic route registration at startup:

// pages.go — New() after registerCoreSurfaces():
func (e *Engine) loadExtensionSurfaces() {
    if e.stores.Surfaces == nil {
        return
    }
    ctx := context.Background()
    surfaces, err := e.stores.Surfaces.List(ctx)
    if err != nil {
        log.Printf("[pages] Failed to load extension surfaces: %v", err)
        return
    }
    for _, sr := range surfaces {
        if sr.Source == "core" {
            continue // Core surfaces already registered
        }
        route, _ := sr.Manifest["route"].(string)
        if route == "" {
            route = "/s/" + sr.ID // Default to /s/:id
        }
        manifest := SurfaceManifest{
            ID:       sr.ID,
            Route:    route,
            Title:    sr.Title,
            Template: "surface-extension",
            Auth:     authFromManifest(sr.Manifest), // default "authenticated"
            Layout:   layoutFromManifest(sr.Manifest),
            Source:   "extension",
        }
        e.surfaces = append(e.surfaces, manifest)
    }
    log.Printf("[pages] Loaded %d extension surfaces from registry",
        len(surfaces) - countCore(surfaces))
}

nginx config — serve extension static assets:

# Extension surface assets
location /surfaces/ {
    alias /data/surfaces/;
    expires 1h;
    add_header Cache-Control "public, immutable";
}

Frontend nav integration:

ListEnabledSurfaces API already returns route + title for all enabled surfaces. The sidebar/nav needs to render extension surfaces as additional items. Extension surfaces appear in a "Surfaces" section of the sidebar, below the core nav items.

// app.js or pages.js — on init:
const surfaces = await App.api.get('/api/v1/surfaces');
surfaces.filter(s => !['chat','admin','settings','editor','notes'].includes(s.id))
    .forEach(s => renderExtensionNavItem(s));

Extension surface manifest contract (.surface archive):

my-dashboard.surface (zip)
├── manifest.json
├── js/
│   └── main.js          ← entry point, mounts into #extension-mount
├── css/
│   └── main.css          ← scoped styles
└── assets/               ← images, fonts, etc.

manifest.json:

{
  "id": "my-dashboard",
  "title": "Dashboard",
  "route": "/s/dashboard",
  "auth": "authenticated",
  "layout": "single",
  "components": ["chat-pane"],
  "hooks": ["surface"]
}

The extension JS receives the standard ctx object (same extension API from v0.11.0) plus access to platform components:

// main.js — extension surface entry point
(function() {
    const mount = document.getElementById('extension-mount');
    const manifest = JSON.parse(
        document.getElementById('extension-surface').dataset.manifest
    );

    // Full access to platform primitives + components
    const chatPane = window.ChatPane?.create(mount, { role: 'assist' });

    // Build custom UI
    mount.innerHTML = '<h1>My Dashboard</h1>';
})();

Admin UI additions:

The admin surfaces section (already exists) gains:

  • Upload button (already wired to InstallSurface)
  • Enable/disable toggles (already wired)
  • Uninstall button (already wired to DeleteSurface)
  • Route display showing the /s/:slug URL

Deliverables:

  • surface-extension template (HTML + CSS + scripts blocks)
  • base.html conditional chain extended with {{if .Manifest}} fallthrough
  • loadExtensionSurfaces() in page engine
  • nginx location block for /surfaces/ static assets
  • Frontend nav renders extension surfaces from ListEnabledSurfaces
  • Admin surfaces section shows upload/enable/disable/uninstall
  • Sample .surface archive (hello-world dashboard) for testing
  • CSP nonce propagation for extension scripts

Phase 2: Workflow Engine Polish

Eight items deferred from v0.26.0. All backend infrastructure exists; these are wiring, UI, and enforcement.

Channel header stage indicator + advance/reject controls

  • Workflow channels show: stage name, step N of M, assignment info
  • Advance / Reject buttons (permission-gated by workflow.manage)
  • Renders in the chat header area (same slot as ai_mode context banner)

Stage persona auto-switch in chat UI

  • On stage transition, chat UI updates the active persona display
  • Model selector reflects the stage persona's bound model
  • System prompt injection already works (v0.26.4); this is FE-only

Assignment notifications via WebSocket

  • New workflow assignment → workflow.assigned WS event to team members
  • Claim confirmation → workflow.claimed WS event to claimer
  • Uses existing notification infrastructure (v0.20.0)

Round-robin auto-assignment

  • Per-stage transition_rules.auto_assign: "round_robin" in workflow_stages
  • On stage entry: query team members, pick least-recently-assigned
  • workflow_assignments.assigned_to populated automatically
  • Falls back to unassigned pool if round-robin fails

on_complete workflow chaining

  • Column exists on workflows table (nullable JSONB, v0.26.1)
  • Schema: {"action": "start_workflow", "target_slug": "...", "data_map": {...}}
  • On workflow completion: if on_complete is non-null, start target workflow with mapped stage_data from completed instance
  • Reuses existing StartInstance() path

Workflow retention enforcement

  • Column exists on workflows table (retention JSONB, v0.26.1)
  • Background sweep (extend staleness goroutine): completed instances older than retention.delete_after_days → hard delete
  • retention.mode = "archive" → set workflow_status = 'archived', ai_mode = 'off' (already implemented for channel archive)

Drag-and-drop stage reorder in builder

  • Backend PATCH /api/v1/workflows/:id/stages/reorder already exists
  • Frontend: HTML5 drag events on stage list items in workflow builder
  • Same DnD pattern as channel/folder reorder (v0.23.1)

Team-scoped workflow management UI

  • Team admin settings surface gains "Workflows" section
  • Lists workflows owned by the team, quick-edit name/description
  • Links to full builder for stage editing
  • Mirrors the admin-level builder but scoped to team_id

Deliverables:

  • Channel header workflow stage indicator component
  • Advance/reject buttons with permission gate
  • Stage persona auto-switch in chat UI
  • workflow.assigned / workflow.claimed WS events
  • Round-robin auto-assignment in stage transition handler
  • on_complete chaining trigger in workflow completion path
  • Retention enforcement in staleness sweep goroutine
  • Drag-and-drop stage reorder in workflow builder UI
  • Team settings → Workflows section

Phase 3: Workspace + Editor Debt

Items deferred from v0.21.x that are low-hanging fruit.

  • .gitignore respect in workspace indexing (workspace/indexer.go — skip paths matching patterns from .gitignore in workspace root)
  • Workspace settings UI: git config section in channel/project settings (remote URL, branch, auto-commit toggle)
  • Pane state persistence per-user/per-project (save pane layout to user_settings JSONB, restore on surface load)
  • Editor drag-drop file reorder (workspace file tree — reorder via DnD, persist order in workspace metadata)

Explicitly deferred (not v0.27.0):

  • User settings git credentials management UI — needs vault-encrypted git credential store (v0.28.0 candidate)
  • Integration tests: clone/commit/push/pull — needs git binary in CI container
  • Article-specific AI tools — needs article surface rebuild
  • Mobile hamburger nav — needs responsive audit pass

Deliverables:

  • .gitignore filter in workspace/indexer.go
  • Git config section in workspace/project settings UI
  • Pane layout persistence in user_settings
  • Editor file tree DnD reorder

ROADMAP Cleanup

Update the ROADMAP itself:

  • Cross off tool grant enforcement (3 locations)
  • Move "Deferred to v0.27.0" items under their proper v0.27.0 section
  • Remove duplicate entries (extension surface routes listed 4× across the file)
  • Add v0.27.0 section with phase structure
  • Update dependency graph

v0.27.1 — Tasks Foundation: Service Channels + Scheduler

The core primitive: a channel with no human participant, driven by a scheduler.

Service Channels

type: 'service' — a new channel type. Service channels:

  • Have zero human participants (only persona participants)
  • Cannot be joined by users (read-only view for authorized users)
  • Are created by the scheduler or the task_create tool
  • Inherit the workflow engine's stage model (optional — simple tasks skip stages entirely)
  • Appear in a "Tasks" sidebar section (collapsed by default)
-- No new migration needed — channel type CHECK already allows extension.
-- Add 'service' to the channel type CHECK constraint:
ALTER TABLE channels DROP CONSTRAINT IF EXISTS channels_type_check;
ALTER TABLE channels ADD CONSTRAINT channels_type_check
    CHECK (type IN ('direct','dm','group','channel','workflow','service'));

Task Definitions

A task is a lightweight scheduled job that creates a service channel and runs a completion (or instantiates a workflow).

tasks table:

CREATE TABLE tasks (
    id            TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
    owner_id      TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    team_id       TEXT REFERENCES teams(id) ON DELETE SET NULL,
    name          TEXT NOT NULL,
    description   TEXT,
    scope         TEXT NOT NULL DEFAULT 'personal'
                  CHECK (scope IN ('personal', 'team', 'global')),

    -- What to run
    task_type     TEXT NOT NULL DEFAULT 'prompt'
                  CHECK (task_type IN ('prompt', 'workflow')),
    persona_id    TEXT REFERENCES personas(id) ON DELETE SET NULL,
    model_id      TEXT,                         -- explicit model override (nullable)
    system_prompt TEXT,                         -- additional system prompt (prepended)
    user_prompt   TEXT,                         -- the message to send
    workflow_id   TEXT REFERENCES workflows(id) ON DELETE SET NULL,
    tool_grants   JSONB,                       -- explicit tool allowlist (nullable = inherit all)

    -- Schedule
    schedule      TEXT NOT NULL,               -- cron expression (5-field)
    timezone      TEXT NOT NULL DEFAULT 'UTC',
    is_active     BOOLEAN NOT NULL DEFAULT true,

    -- Execution policy
    max_tokens       INTEGER NOT NULL DEFAULT 4096,
    max_tool_calls   INTEGER NOT NULL DEFAULT 10,
    max_wall_clock   INTEGER NOT NULL DEFAULT 300,  -- seconds
    output_mode      TEXT NOT NULL DEFAULT 'channel'
                     CHECK (output_mode IN ('channel', 'note', 'webhook')),
    output_channel_id TEXT REFERENCES channels(id) ON DELETE SET NULL,
    webhook_url      TEXT,

    -- Provider routing
    provider_config_id TEXT REFERENCES provider_configs(id) ON DELETE SET NULL,

    -- Notifications
    notify_on_complete BOOLEAN NOT NULL DEFAULT false,
    notify_on_failure  BOOLEAN NOT NULL DEFAULT true,

    -- Bookkeeping
    last_run_at   TIMESTAMPTZ,
    next_run_at   TIMESTAMPTZ,
    run_count     INTEGER NOT NULL DEFAULT 0,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_tasks_next_run ON tasks (next_run_at) WHERE is_active = true;
CREATE INDEX idx_tasks_owner ON tasks (owner_id);

Two task types:

  1. Prompt task (task_type = 'prompt'): Create a service channel, send user_prompt with system_prompt prepended, collect response. Simple, single-turn. Good for: news digest, stock screener, daily standup prep, report generation.

  2. Workflow task (task_type = 'workflow'): Instantiate a workflow in a service channel. Multi-stage, tool-using, potentially long-running. Good for: data pipeline, automated review, research agent.

Scheduler

Background goroutine (same pattern as compaction scanner, staleness sweep):

type TaskScheduler struct {
    stores   store.Stores
    interval time.Duration // poll interval: 30s
    stop     chan struct{}
}

func (s *TaskScheduler) Run() {
    ticker := time.NewTicker(s.interval)
    for {
        select {
        case <-ticker.C:
            s.poll()
        case <-s.stop:
            return
        }
    }
}

func (s *TaskScheduler) poll() {
    // SELECT * FROM tasks WHERE is_active AND next_run_at <= now()
    // ORDER BY next_run_at LIMIT 10
    due, _ := s.stores.Tasks.ListDue(ctx)
    for _, task := range due {
        go s.execute(task)
    }
}

Cron parsing: use github.com/robfig/cron/v3 for 5-field expressions. Compute next_run_at after each execution.

Provider Resolution for Tasks

Task execution needs a provider. Resolution order:

  1. Explicit provider_config_id on task (user chose a specific provider)
  2. Personal BYOK provider (if scope = 'personal' and user has BYOK keys)
  3. Team provider (if scope = 'team')
  4. Global provider (fallback)
  5. Routing policy (if task's model matches a routing policy)

This is the same resolution chain as completions, reusing the existing resolveProvider() path in completion.go.

Migration

  • 026_tasks.sql: tasks table, service channel type extension

Deliverables:

  • tasks table + store interface + PG/SQLite implementations
  • type: 'service' channel type
  • TaskScheduler background goroutine
  • Cron expression parsing + next_run_at computation
  • Task execution path (create service channel, run completion)
  • Provider resolution for task context
  • Tasks sidebar section (read-only view of service channels)

v0.27.2 — Task Execution: Budgets + Admin Controls

Execution Budgets

Three limits enforced in the task execution path:

  1. max_tokens — total output tokens. Tracked via existing usage_log infrastructure. Execution halts mid-stream if exceeded.
  2. max_tool_calls — total tool invocations per run. Counter incremented in streamWithToolLoop. Execution halts on breach.
  3. max_wall_clock — seconds. context.WithTimeout on the execution goroutine. Hard kill on timeout.

Budget breach → task marked as budget_exceeded in run history, notification to owner.

Task Run History

task_runs table:

CREATE TABLE task_runs (
    id           TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
    task_id      TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
    channel_id   TEXT REFERENCES channels(id) ON DELETE SET NULL,
    status       TEXT NOT NULL DEFAULT 'running'
                 CHECK (status IN ('running','completed','failed',
                        'budget_exceeded','cancelled')),
    started_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at TIMESTAMPTZ,
    tokens_used  INTEGER DEFAULT 0,
    tool_calls   INTEGER DEFAULT 0,
    wall_clock   INTEGER DEFAULT 0,  -- seconds
    error        TEXT
);

Admin Controls

Global config keys:

  • tasks.enabled — master kill switch (default: true)
  • tasks.allow_personal — whether non-admin users can create personal tasks (default: false). Same pattern as personas.allow_personal.
  • tasks.max_concurrent — max simultaneous task executions (default: 5)
  • tasks.default_budget.max_tokens — default ceiling (overridable per task)
  • tasks.default_budget.max_tool_calls — default ceiling
  • tasks.default_budget.max_wall_clock — default ceiling (seconds)

Admin panel — Tasks section:

  • List all tasks (filterable by owner, team, scope, status)
  • View task run history with budget usage
  • Pause/resume individual tasks
  • Kill running executions
  • Edit default budgets
  • Toggle tasks.allow_personal

Permission integration:

  • New permission: tasks.create — required to create tasks
  • New permission: tasks.admin — required to manage others' tasks
  • Everyone group gets tasks.create if tasks.allow_personal is true

Deliverables:

  • Execution budget enforcement (tokens, tool calls, wall clock)
  • task_runs table + store
  • Budget breach notification to task owner
  • Global config keys for task admin controls
  • Admin panel Tasks section (CRUD, history, kill switch)
  • tasks.create and tasks.admin permissions
  • tasks.allow_personal toggle (mirrors personas.allow_personal)

v0.27.3 — Task Chaining: Webhooks + Workflow-to-Workflow

Completion Webhooks

When a task (or workflow) completes, optionally POST to an external URL:

type WebhookPayload struct {
    TaskID      string    `json:"task_id"`
    TaskName    string    `json:"task_name"`
    Status      string    `json:"status"`
    ChannelID   string    `json:"channel_id"`
    CompletedAt time.Time `json:"completed_at"`
    Output      string    `json:"output"`       // last assistant message
    StageData   any       `json:"stage_data"`   // workflow stage data (if applicable)
}
  • Webhook URL on task or workflow (webhook_url column)
  • Retry: 3 attempts with exponential backoff (1s, 5s, 25s)
  • Timeout: 10s per attempt
  • HMAC signature header (X-Switchboard-Signature) using a per-task secret

task_create Tool

AI-invocable tool that spawns sub-tasks:

{
  "name": "task_create",
  "description": "Create a new scheduled or one-shot task",
  "parameters": {
    "name": "string",
    "prompt": "string",
    "schedule": "string (cron or 'once')",
    "persona": "string (handle, optional)",
    "max_tokens": "integer (optional)"
  }
}
  • RequireWorkflow or RequireTeam predicate (not available in personal chats — prevents runaway task creation)
  • One-shot tasks: schedule = 'once', next_run_at = now()
  • Created tasks inherit the parent's scope (team/global)
  • Depth limit: tasks cannot create tasks (no recursive spawning)

Workflow-to-Workflow Chaining

Wires the on_complete column (v0.26.1) into the task system:

  • On workflow completion with on_complete set: create a one-shot task that instantiates the target workflow with mapped data
  • Data mapping: data_map keys in on_complete JSONB map source stage_data fields to target workflow's initial stage_data

Deliverables:

  • Webhook delivery with retry + HMAC signature
  • task_create tool with depth limit
  • on_complete chaining via task system
  • Webhook secret generation per task/workflow

v0.27.4 — Personal Tasks: BYOK Scheduling + User Task UI

The user-facing task experience. Users create personal tasks that run against their BYOK providers.

Use Cases

  • Morning news digest: Daily at 6am, prompt "Summarize top tech news", output to a personal service channel. Persona: "News Analyst" with web_search tool grant.
  • Stock screener: Weekdays at market open, prompt "Check my watchlist for unusual pre-market activity", BYOK OpenAI key for cost control.
  • Weekly project summary: Friday at 5pm, prompt "Summarize this week's activity across my projects", uses conversation_search + workspace_search tools.
  • Daily standup prep: Weekday mornings, reviews yesterday's notes and generates standup talking points.

Settings Surface — Tasks Section

New section in user settings (same pattern as BYOK, Personas):

  • Task list: name, schedule (human-readable), last run status, next run
  • Create task: name, persona picker, model picker, prompt editor, schedule builder (preset crons + custom), budget overrides
  • Task detail: run history, output channel link, edit, pause/delete
  • BYOK indicator: shows which provider the task will use

Schedule Builder

Preset schedules + custom cron for power users:

Preset Cron
Every morning (6am) 0 6 * * *
Weekday mornings (8am) 0 8 * * 1-5
Every hour 0 * * * *
Weekly (Monday 9am) 0 9 * * 1
Monthly (1st at midnight) 0 0 1 * *
Custom... user-entered 5-field cron

Timezone selector defaults to browser timezone.

BYOK Task Routing

Personal tasks prefer the user's BYOK provider:

  1. If task has explicit provider_config_id → use it
  2. If user has a BYOK provider config for the task's model → use BYOK
  3. Fall through to team/global provider (if allowed by admin)

Admin can restrict personal tasks to BYOK-only via tasks.personal_require_byok config key (default: false). When true, personal tasks without a BYOK provider fail with a clear error instead of falling through to org providers.

Output Modes

Three output destinations:

  1. Channel (default): Output goes to a persistent service channel. User views it like a read-only chat. Channel accumulates run outputs over time (scrollable history).
  2. Note: Output saved as a channel-scoped note (good for structured data, reports). Note title includes timestamp.
  3. Webhook: Output POSTed to external URL (for integration with other tools — Slack, email, etc.).

Task Sidebar Section

New sidebar section (below Channels, above Chats):

▾ Tasks             (collapsible)
    ◷ Morning News Digest    ✓ 6:02am
    ◷ Stock Screener         ✓ 9:30am
    ◷ Weekly Summary         ⏳ Fri 5pm
  + New task

Click opens the task's output channel. Status indicators: ✓ (last run succeeded), ✗ (failed), (next run time), ▶ (running now).

Deliverables:

  • Settings → Tasks section (CRUD, schedule builder, budget config)
  • BYOK provider routing for personal tasks
  • tasks.personal_require_byok config key
  • Output modes: channel, note, webhook
  • Tasks sidebar section with status indicators
  • "Run Now" button for manual trigger
  • Task output channel read-only view

v0.28.0 — Platform Polish (TBD Pull-Forward)

Candidates pulled from the TBD section, prioritized by value and dependency readiness.

Tier 1 — High value, dependencies met

Virtual scroll for long conversations Conversations with 500+ messages bog down the DOM. Virtual scroll renders only visible messages + buffer zone. Prerequisite for heavy task output channels.

KB auto-injection (context-aware) Top-K chunk prepend to system prompt, context budget aware, per-channel toggle. Useful for tasks that need domain knowledge without explicit kb_search tool calls. Latency budgeting required (embedding lookup adds ~200ms).

Helm chart Raw k8s manifests with ${VAR} substitution → proper Helm chart. values.yaml for replicas, image tags, ingress, storage, secrets, feature flags. Subchart for dev/test Postgres. Target: helm install switchboard ./chart. Now makes sense because the feature set is stabilizing post-tasks.

Per-provider model preferences user_model_settings unique key is (user_id, model_id) — same model from different providers shares one visibility toggle. Needs provider_config_id dimension in DB constraint, store, API, and frontend hiddenModels keying. Natural fit now that BYOK tasks need per-provider model selection.

Tier 2 — Medium value, some design work needed

Memory compaction Summarize old memories to save context tokens. Confidence decay: reduce confidence over time, prune low-confidence entries. Natural extension of the memory system (v0.18.0).

capability_match routing policy "Cheapest model with tool_calling" — a new routing policy type. Useful for tasks that need tool use but don't need the strongest model.

New provider types via config file OpenAI-compatible endpoints registrable via YAML config (no Go code). Enables users to point at local LLMs (Ollama, vLLM, llama.cpp) without a provider code change.

Tier 3 — Future (v0.29+)

  • Desktop app (Tauri) — large scope, independent track
  • Full PWA with offline — needs service worker rewrite
  • Plugin/extension marketplace — needs extension surface routes first (v0.27.0)
  • Starlark/sidecar extension tiers — needs extension surface routes first
  • Git credentials vault store — needs vault extension design
  • Cross-persona memory sharing — needs memory system redesign

Design Decision: Task Permission Model

Tasks introduce a new resource type that intersects with multiple existing permission boundaries:

Scope Who can create Provider used Visibility
Personal User (if tasks.allow_personal) User's BYOK or team fallback Owner only
Team User with tasks.create + team membership Team provider Team members
Global Admin Global provider All users (read)

Personal tasks are the BYOK sweet spot. The admin toggle (tasks.allow_personal) mirrors the existing personas.allow_personal pattern — a single boolean in global config, surfaced in the admin settings panel.

Why not just a permission? The tasks.create permission already gates the ability. tasks.allow_personal is a policy control — admins might allow task creation for team tasks but prohibit personal tasks to prevent uncontrolled BYOK spending. Two orthogonal knobs.


Design Decision: Task vs. Workflow Relationship

Tasks and workflows are related but distinct:

Task Workflow
Trigger Cron schedule or task_create tool Human click or task trigger
Participants Zero humans (service channel) Humans + AI
Complexity Single prompt or workflow instantiation Multi-stage, assignment queue
Output Channel, note, or webhook Channel (interactive)
Lifecycle Recurring (cron) or one-shot Single instance

A prompt task is simpler than a workflow — it's a scheduled completion. A workflow task is a task that instantiates a workflow. This keeps the task system lean (scheduling + budgets + output routing) and the workflow engine rich (stages + assignment + personas).

The task_type column makes this explicit. Prompt tasks don't need workflows at all — they're a direct scheduler → completion path. Workflow tasks reuse the full workflow engine.


Resolved Decisions

  1. Task output retention. Use channel-level retention from v0.26.0. No per-task retention policy needed — the channel's retention JSONB handles archive/delete semantics. Notes created by task output survive channel removal (notes are channel-scoped but not cascade-deleted). Storage quotas per user are a future concern (v0.29+ candidate) — not blocking for initial task support.

  2. Concurrent task execution. Skip with a warning log. If task_runs has a row with status = 'running' for the task, the scheduler skips that tick. Log: [scheduler] Skipping task %s — previous run still active. No queue — if a task consistently overruns its schedule, the user needs to adjust the cron interval or budget.

  3. Task templates. Ship 35 optional starter templates in v0.27.4. Presented as suggestions during task creation ("Start from a template..." option alongside blank task). Not auto-created — user must explicitly choose one. Templates are just pre-filled form values, not persistent DB records.

  4. Task notifications. Opt-in per task. Two booleans on the tasks table: notify_on_complete (default false), notify_on_failure (default true). Uses existing notification infrastructure (v0.20.0 bell + WebSocket push).