Changeset 0.28.0.1 (#173)
This commit is contained in:
842
docs/archive/DESIGN-0.27.0.md
Normal file
842
docs/archive/DESIGN-0.27.0.md
Normal file
@@ -0,0 +1,842 @@
|
||||
# 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}}<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 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).
|
||||
Reference in New Issue
Block a user