# 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:928–949` — second-pass allowlist via `GetToolGrants()` | v0.25.0, v0.26.0 | | Version snapshot includes persona tool grants | `workflows.go:263–273` — `stageSnapshot.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 61–66 and 118–122 of `base.html` use `{{if eq .Surface "chat"}}` chains. Extension surfaces fall through to "Unknown surface". Same pattern for CSS includes (line 25–26) and script includes (118–122). **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}}
Unknown surface: {{.Surface}}
{{end}} ``` New template `templates/surfaces/extension.html`: ```html {{define "surface-extension"}}
{{/* Extension JS mounts into this container */}}
{{end}} {{define "css-extension"}} {{/* Extension CSS loaded dynamically from surfacesDir */}} {{if .Manifest}} {{end}} {{end}} {{define "scripts-extension"}} {{if .Manifest}} {{end}} {{end}} ``` **Dynamic route registration at startup:** ```go // 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:** ```nginx # 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. ```js // 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`: ```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: ```js // 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 = '

My Dashboard

'; })(); ``` **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) ```sql -- 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:** ```sql 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): ```go 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:** ```sql 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: ```go 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: ```json { "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 3–5 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).