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/DESIGN-0.27.0.md
2026-03-10 16:38:06 +00:00

843 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:263273``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 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`:
```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:**
```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 = '<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)
```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 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).