Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
74e1ca1
v2: events page — activity feed and subscription management
xBalbinus Aug 10, 2026
3f1eac8
v2: dev scripts to seed and smoke the webhook event pipeline
xBalbinus Aug 11, 2026
40d571e
v2: events smoke script cleans up its subscription and workflow
xBalbinus Aug 11, 2026
a920165
v2: triggers UI, team-owned workflow creation, role-aware teams panel
xBalbinus Aug 11, 2026
325f5d4
Merge branch 'feat/v2-team-triggers-ui' into feat/v2-surface-ui
xBalbinus Aug 11, 2026
00f915d
web: extract shared primitives; split events and trigger panels
xBalbinus Aug 11, 2026
e91e613
v2: schedule/webhook route correctness fixes
xBalbinus Aug 11, 2026
8716549
v2: gate event-subscription mutations to their owner
xBalbinus Aug 11, 2026
9638a90
v2: fix dev scripts' PGlite typecheck break and guessed webhook shape
xBalbinus Aug 11, 2026
263b404
v2: shared OwnerPicker/SelectMenu/copy-hook, ARIA tab keyboard nav
xBalbinus Aug 11, 2026
0cdef21
v2: adopt OwnerPicker; fix trigger-panel error/confirm gaps
xBalbinus Aug 11, 2026
6021384
v2: events-page fixes — filter dropdowns onto SelectMenu, gate
xBalbinus Aug 11, 2026
1ae0474
address review: scope the schedule delete to the checked predicate
xBalbinus Aug 11, 2026
1fdba3c
v2: retarget the design tokens onto a brand blue, drop the unused serif
xBalbinus Aug 11, 2026
a3f5555
v2: componentize the workflow inspector, dedupe date/copy helpers, ag…
xBalbinus Aug 11, 2026
c0d30c0
v2: forward thinking message parts through the wire, render collapsed
xBalbinus Aug 12, 2026
3f1aff8
v2: shared CodeBlock with real syntax highlighting
xBalbinus Aug 12, 2026
e908f56
v2: elapsed-time counter on the turn status badge
xBalbinus Aug 12, 2026
64c7929
v2: surface team ownership on skill sources
xBalbinus Aug 12, 2026
3bbaa51
v2: derive workflow schedule owner from the workflow, not hardcoded user
xBalbinus Aug 12, 2026
89300dd
v2: team-scoped session read access + orchestrator UI
xBalbinus Aug 12, 2026
3bf0580
v2: authorize team session lifecycle by team, not by first opener
xBalbinus Aug 12, 2026
ea2ec12
v2: team assistants become siblings of your own in the chat sidebar
xBalbinus Aug 12, 2026
9a75424
v2: one OwnerBadge, linking each owned row to its team's assistant
xBalbinus Aug 12, 2026
b1e802d
v2: delete dead code and UI that does not earn its place
xBalbinus Aug 12, 2026
90f5c88
v2: give success and danger badges a background again, add warning
xBalbinus Aug 12, 2026
89c7853
v2: route a decision gate to whoever can answer it
xBalbinus Aug 12, 2026
98a7830
v2: one run-state vocabulary, and honour initialPrompt
xBalbinus Aug 12, 2026
b29370a
v2: let the composer interrupt a working agent, honestly labelled
xBalbinus Aug 12, 2026
57e1a30
v2: make a sound when the assistant is waiting on you
xBalbinus Aug 12, 2026
421cca5
v2: wire fields for connection health, delivery retry, and service icons
xBalbinus Aug 12, 2026
b40e650
v2: make the sessions list live and readable at a glance
xBalbinus Aug 13, 2026
0bfde36
v2: real service marks, and stop showing green for a dead credential
xBalbinus Aug 13, 2026
65f34e1
v2: let a failed event be redelivered, and say when it retries
xBalbinus Aug 13, 2026
24e47f6
v2: memory search shows the matching text, and links go somewhere
xBalbinus Aug 13, 2026
e1c8a05
v2: make the chat sidebar survive many teams and many threads
xBalbinus Aug 13, 2026
0bb52e4
v2: prove two members can share one team orchestrator
xBalbinus Aug 13, 2026
6b24104
v2: assistants become first-class rows a principal owns
xBalbinus Aug 13, 2026
7ca8413
v2: wait for an assistant's session before opening the conversation
xBalbinus Aug 14, 2026
7903ead
v2: a workspace switcher beside the logo, and one workspace in the rail
xBalbinus Aug 14, 2026
580ee29
v2: a session can belong to a team, and the list can be read per work…
xBalbinus Aug 14, 2026
4ead037
v2: a team can own event subscriptions and schedules
xBalbinus Aug 14, 2026
a925c40
v2: move the sidebar toggle into the nav, off the sidebar it covered
xBalbinus Aug 14, 2026
ea0929e
v2: the workspace scopes every surface, so the Owner selects go
xBalbinus Aug 14, 2026
80d7415
v2: workflows, skills and skill sources list by workspace
xBalbinus Aug 14, 2026
ce776df
chore: ignore .env backups, which hold the same secrets
xBalbinus Aug 14, 2026
65db0e8
Merge dev-v2 into feat/v2-surface-ui
xBalbinus Aug 14, 2026
487dd68
v2: fix review findings — team access, ping suppression, scope race
xBalbinus Aug 14, 2026
612d12f
v2: say what a connection gives away before the OAuth redirect
xBalbinus Aug 16, 2026
4b0fc4d
fix: let .env decide the dev auth mode
xBalbinus Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ wrangler.deploy.toml

# Environment files
.env
.env.backup*
.env.e2e
.env.local
.env.*.local
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ dev-api-node: ## Start the new Node API (@valet/api) on :8788
@echo "$(GREEN)Starting @valet/api on :8788$(NC)"
@set -a; [ -f .env ] && . ./.env; set +a; \
if [ -z "$$ANTHROPIC_API_KEY" ]; then echo "$(RED)ANTHROPIC_API_KEY is required (env or .env)$(NC)"; exit 1; fi; \
cd packages/api && VALET_LOCAL_AUTH=1 PORT=8788 $(PNPM) run dev
cd packages/api && VALET_LOCAL_AUTH=$${VALET_LOCAL_AUTH:-1} PORT=8788 $(PNPM) run dev

dev-web: ## Start the new web client (@valet/web) on :5173
@echo "$(GREEN)Starting @valet/web on :5173 (proxy → :8788)$(NC)"
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ Wire event types are listed in
| `/api/org/invites` | GET / POST / DELETE | Invites (admin) |
| `/api/org/llm-providers` | GET / POST / DELETE | BYO LLM provider keys (admin) |
| `/api/org/github-app` | — | GitHub App manifest setup (admin) |
| `/api/org/image-catalog`, `/api/org/prebuilds` | — | Sandbox base images and prebuild configs (admin) |
| `/api/prebuilds/for-repo` | GET | Prebuild lookup for a repo (member) |
| `/api/org/sources` | GET / POST / PATCH / DELETE | Sandbox image sources and their bakes (admin) |
| `/api/sources/for-repo` | GET | Newest repo image bake for a repo (member) |
| `/api/admin` | — | Operator submission surface (admin) |

## System
Expand Down
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ single pre-1.0 `0000` migrations, edited in place.
| Identity & auth | `orgs`, `user`, better-auth tables (`session`, `account`, `verification`, `sso_provider`, `apikey`, `oauth_application`, `oauth_access_token`, `oauth_consent`), `org_members`, `invites`, `sandbox_tokens` |
| Sessions & UI | `agent_sessions`, `session_threads`, `messages` |
| Teams | `teams`, `team_members` |
| Orchestrator & attention | `orchestrator_identities`, `child_watches`, `notifications`, `user_notification_preferences`, `event_drop_log` |
| Orchestrator & attention | `assistants`, `child_watches`, `notifications`, `user_notification_preferences`, `event_drop_log` |
| Channels | `channel_bindings`, `user_identity_links`, `identity_link_codes` |
| Memory | `memory_files` (tsvector full-text search) |
| Workflows | `workflow_definitions`, `workflow_runs`, `workflow_checkpoints`, `workflow_signals`, `action_invocations` |
Expand Down Expand Up @@ -346,8 +346,10 @@ Each user's orchestrator is a full engine session — same threads, queue, and
gates — with a persona, memory tools (`mem_*` over the `memory_files` tree),
a daily journal, and a `childSpawner` for launching child coding sessions
(depth-limited to 1). It runs sandbox-less. An **attention router** turns
stuck submissions and child decision gates into notifications, delivered
in-app and DM'd over linked channels.
stuck submissions and pending decision gates into notifications, delivered
in-app and DM'd over linked channels. A gate goes to the owner who can answer
it: the parent's owner for a child session, the session's own owner for every
other session.

## Workflows

Expand Down
2 changes: 1 addition & 1 deletion docs/plans/2026-07-14-workflow-nodes-and-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
6. **tool executor:** intent (effects: `{ invocationId }`) → `invokeAction` with `invocationId = 'workflow:{runId}:{nodeId}[:{iteration}]'` → `{ok:true}` → completed with `result`; `{ok:false}` → node failed with the error. Re-entry with intent re-invokes with the SAME invocationId (receiver-side dedup is the contract; the stub is trivially idempotent).
7. **Executor-interface iteration/alias support:** `NodeExecutorArgs` gains `iteration: number` (default 0) and `aliases?: Record<string, unknown>` merged into the template context by the shared context builder. All existing executors thread `iteration` through their checkpoint writes instead of the hardcoded 0 and append `[:{iteration}]` (iteration > 0 only) to their dispatchIds/invocationIds/session ids — closing the Phase-5 "dispatchId omits iteration" minor. Existing tests must pass unchanged (iteration defaults to 0 → identical ids).
8. **foreach executor:** items = template-resolve `node.items` (must be an array; else node failed); truncate to `maxItems` (default 100), record `truncatedCount`. Per item i: invoke the registry executor for `body.type` with `iteration: i`, `aliases: { [itemAlias ?? 'item']: items[i], [indexAlias ?? 'index']: i }`, checkpoints keyed `(runId, body.id, i)` (the body id is not in `definition.nodes`, so the wave loop never sees these rows — the foreach owns them). Body executors that park bubble their `waitingOn` entries up through the foreach's parked result; on re-drive the foreach skips iterations with terminal body checkpoints and re-enters intent-holding ones. Concurrency window = `concurrency` (default 1): keep up to N iterations in flight (dispatched-and-parked counts as in flight). `onItemError`: 'fail' (default) → first failed iteration fails the foreach (remaining un-started iterations are not started; in-flight parked ones are aborted by the foreach itself (best-effort `engine.abort` over their persisted receipts) before it returns failed); 'skip'/'collect' → record per-item status and continue. Aggregate iteration-0 checkpoint result = main's `ForeachResult` shape `{ items: [{status, data?, error?}], count, inputCount, truncatedCount, completedCount, skippedCount, failedCount }`; foreach node status: completed unless onItemError 'fail' tripped (then failed). Foreach itself is never a body (validator).
9. **Editor model (`packages/web/src/components/workflows/editor-model.ts`):** lift main's `workflow-editor-model.ts` + its test, trimmed to the v2 node set and v2 `WorkflowDefinition` (no NODE_DOCS dependency — v2 keeps a small local `NODE_META: Record<DagNodeType, { label, description, defaultNode(id) }>`). Keep: definition↔flow conversion (nodes/edges with `fromOutput` handles on if/approval sources), add/remove/duplicate node, connect rules (single trigger root, no self-edges, fromOutput only from if/approval), position persistence into `definition.ui.nodes[id].position`, viewport into `definition.ui.viewport`, dirty tracking, id generation (slugified unique). Auto-layout for definitions without `ui`: BFS-depth layered layout (column = depth, row = index within depth; 260×120 spacing) — no dagre/elk dependency.
9. **Editor model (`packages/web/src/components/workflows/editor-model.ts`):** lift main's `workflow-editor-model.ts` + its test, trimmed to the v2 node set and v2 `WorkflowDefinition` (no NODE_DOCS dependency — v2 keeps a small local `NODE_META: Record<DagNodeType, { label, description, defaultNode(id) }>`). Keep: definition↔flow conversion (nodes/edges with `fromOutput` handles on if/approval sources), add/remove/duplicate node, connect rules (single trigger root, no self-edges, fromOutput only from if/approval), position persistence into `definition.ui.nodes[id].position`, viewport into `definition.ui.viewport`, id generation (slugified unique). The editor model stays a set of pure definition-in/definition-out functions; the editor component holds the definition and the dirty flag in React state. Auto-layout for definitions without `ui`: BFS-depth layered layout (column = depth, row = index within depth; 260×120 spacing) — no dagre/elk dependency.
10. **Editor presentation (`packages/web/src/components/workflows/editor/`):** `@xyflow/react` ^12 (new dep in packages/web). Custom node component in calm-companion tokens (paper card, line border, moss accent for the selected node, node-type label + summary line, amber badge for validation errors on that node); if/approval sources render two labeled source handles (true/false); default edge with `when`-condition badge when set. Palette: a left rail of "add node" buttons (one per addable type — everything except trigger). Inspector: right panel editing the selected node's fields — per-type forms (session: prompt/title/model/outputSchema-as-JSON-textarea/wait-mode; llm: model/system/prompt/schema/temperature/maxTokens; approval: prompt/summary/details/timeout/onDeny; wait: duration; if: condition fields per the v2 `IfNode` shape (read `packages/workflow/src/dag/nodes.ts` for the exact condition structure and mirror main's inspector fields for it); set: key/value template rows; foreach: items/aliases/maxItems/concurrency/onItemError + a nested body sub-form (body type selector + that type's form, reusing the same field components); tool: service/action/params-JSON/summary; stop: outcome; trigger: read-only). Edge selection edits `when` + `fromOutput`. Validation: run `validate()` from `@valet/workflow` on every change; banner lists errors; nodes named in errors get the amber badge. Save = PUT with the full definition (including `ui`); unsaved-changes indicator; Cancel-discard.
11. **Routes restructure:** `/workflows` index keeps the list (name, run count, Run button) but each definition links to `/workflows/$workflowId` — the editor page (canvas + inspector + Save + Run + runs list in a collapsible footer or side section). The JSON textarea remains as an "Edit JSON" toggle on the editor page (switching modes round-trips through the same definition state; JSON mode is the escape hatch and the create-form default stays JSON-free: "New workflow" now creates a minimal `trigger → stop` definition immediately and navigates to the editor). `/workflows/runs/$runId` unchanged.
12. **Editor testing:** the lifted model gets its (adapted) unit tests — conversion round-trip incl. `ui` positions, connect rules, add/remove, fromOutput handles. Presentation gets focused component tests: palette click adds a node to the model; inspector field edit updates the definition; validation banner appears for a broken definition; Save fires PUT with the edited definition. jsdom needs a `ResizeObserver` mock for xyflow — add to test setup if absent. Full-canvas interaction (drag/connect gestures) is NOT unit-tested — covered by the manual dogfood.
Expand Down
24 changes: 16 additions & 8 deletions docs/specs/2026-07-11-orchestrator-engine-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

This spec covers:

- Orchestrator identity: user, team, and org orchestrators as Principals, well-known session IDs
- Orchestrator identity: user, team, and org principals own assistants, each with its own session ID
- Lifecycle on the engine: lazy creation, instant wake, sandbox-less default
- Channel bindings: the tenancy-resolution model the engine's ingress pipeline delegates to
- Routing policy: DMs vs team-bound surfaces vs org surfaces, unbound events
Expand All @@ -28,21 +28,29 @@ This spec covers:

## Identity

Ownership everywhere is a **Principal** — `{ type: 'user' | 'team' | 'org'; id }`, serialized `${type}:${id}`. Orchestrator session IDs are `orchestrator:{type}:{id}`, produced and parsed only through the shared principal helpers (`orchestratorSessionId` / `parseOrchestratorSessionId`) — never ad-hoc prefix checks. Colon-free URL aliases exist per kind (`orchestrator`, `team-orchestrator-{teamId}`).
Ownership everywhere is a **Principal** — `{ type: 'user' | 'team' | 'org'; id }`, serialized `${type}:${id}`.

A principal owns any number of **assistants**, and an assistant, not the principal, holds the session. Assistant session IDs are `assistant:{assistantId}`, produced and parsed only through the shared helpers (`assistantSessionId` / `parseAssistantSessionId`) — never ad-hoc prefix checks. The principal is the assistant's owner and scope; it is no longer the session's address. `docs/specs/2026-08-13-assistants-design.md` supersedes the one-orchestrator-per-principal model this section originally described, and is the contract for the `assistants` table, the default assistant and the routes.

The word "orchestrator" in the rest of this document means an assistant session — `purpose: 'orchestrator'` on the engine session, and the routes that still carry the name.

Session rows carry `owner: Principal` **plus an actor** (`userId` = the human whose action created or triggered the work). For team/org-owned sessions, owner says who the work belongs to; actor preserves per-human attribution on every prompt, memory write, and credential use.

Three orchestrator kinds, all full engine sessions with `purpose: 'orchestrator'`:

| | User orchestrator | Team orchestrator | Org orchestrator |
|---|---|---|---|
| Session ID | `orchestrator:user:{userId}` | `orchestrator:team:{teamId}` | `orchestrator:org:{orgId}` |
| Session ID | `assistant:{assistantId}` | `assistant:{assistantId}` | `assistant:{assistantId}` |
| Steered by | owner only | any team member; team admins get owner-level lifecycle | org admins; members converse via bound surfaces |
| Receives | DMs, personal schedules, signals addressed to the user | team channel bindings, team schedules, team workflow dispatch | org-wide surfaces, unattributed events, org automation |
| Memory scope | `user:{userId}` | `team:{teamId}` | `org:{orgId}` |
| Default queue mode | `steer` | `followup` | `followup` |

Session IDs are stable and permanent — one durable identity per orchestrator, never rotated. `orchestrator_identities` rows are unique on `(orgId, ownerType, ownerId)`; handles are unique per org. The orchestrator persona is owner-kind-aware: a team orchestrator's persona states that it serves multiple people and attributes statements to actors; it is not a personal persona with a different name.
Session IDs are stable and permanent — one durable identity per assistant, never rotated. `assistants` rows are unique on `session_id`, and a PARTIAL unique index on `(org_id, owner_type, owner_id) WHERE is_default` holds exactly one default per principal. The default is what a caller that named only a principal means: a workflow `orchestrator` node, an event subscription and a channel binding all resolve principal → default assistant, because none of them has a basis for choosing between several. A human picks; automation gets a stable target.

Every path that turns a principal into a session goes through `resolveDefaultAssistant` (`packages/api/src/assistants/service.ts`), which creates the default on first use. Nothing derives a session ID from a principal.

The assistant persona is owner-kind-aware: a team assistant's persona states that it serves multiple people and attributes statements to actors; it is not a personal persona with a different name.

**Teams** are the org's membership structure: `teams` (names unique per org) + `team_members` (`role: 'admin' | 'member'`), with atomic last-admin guards on role change and removal, creator auto-admitted as admin, and deletion blocked while team-owned workflows exist. Team membership is the sole access path to team-owned resources (sessions, memory, credentials, workflows).

Expand All @@ -52,7 +60,7 @@ The org orchestrator is the org's chief of staff — the responder for org-wide

### Lazy creation

An orchestrator session is created on first demand — first channel binding, first web visit to the orchestrator UI, first scheduled dispatch. Creation is idempotent by well-known ID (`engine.createSession({ id })` returns the existing session). There is no onboarding step that must succeed before events can flow; an event arriving for a not-yet-created orchestrator creates it inline.
An assistant session is created on first demand — first channel binding, first web visit to the assistant UI, first scheduled dispatch. Each of those names a principal, so each resolves that principal's default assistant first (creating the row if this is the first use), then creates the session idempotently by the assistant's own ID (`engine.createSession({ id })` returns the existing session). There is no onboarding step that must succeed before events can flow; an event arriving for a not-yet-created assistant creates it inline.

### Instant wake

Expand All @@ -77,7 +85,7 @@ Consequences worth stating:

### Health

The engine's submission machinery replaces the orchestrator-specific reconcile/backoff apparatus. A crashed turn is reconciled by the submission decision tree; a stuck sandbox is a failed tool call plus background re-provision. The remaining application-level check is a reconcile sweep that verifies every `orchestrator_identities` row has a live session row, and re-creates lazily on drift.
The engine's submission machinery replaces the orchestrator-specific reconcile/backoff apparatus. A crashed turn is reconciled by the submission decision tree; a stuck sandbox is a failed tool call plus background re-provision. The remaining application-level check is a reconcile sweep that verifies every live `assistants` row has a session row, and re-creates lazily on drift.

## Channel Bindings and Routing

Expand All @@ -92,7 +100,7 @@ interface ChannelBinding {
channelType: string; // 'slack' | 'telegram' | ...
conversationKey: string; // transport codec output, e.g. slack:v1:{team}:{channel}[:{threadTs}]
owner: Principal; // user:{id} | team:{id} | org:{id}
sessionId: string; // orchestrator:{type}:{id} | a specific session
sessionId: string; // assistant:{assistantId} | a specific session
threadKeyTemplate: string; // how external threads map to engine thread keys
queueMode: QueueMode; // default per owner kind: user 'steer', team/org 'followup'
triggerMode: 'mention' | 'all'; // shared surfaces: respond only when mentioned (or in an active thread) vs everything
Expand Down Expand Up @@ -208,7 +216,7 @@ Team-owned sessions resolve credentials by **reference, not copy**: a team crede

Membership is the only access path to team-owned resources — no creator shortcut, no participant grants, no org-visible fallback:

- **Team sessions**: current membership grants `collaborator` (view + prompt); team `admin` grants `owner` (hibernate/delete/restart/bindings). Non-members receive not-found, indistinguishable from a nonexistent session.
- **Team sessions**: current membership grants `collaborator` (view + prompt); team `admin` grants `owner` (hibernate/delete/restart/bindings). Non-members receive not-found, indistinguishable from a nonexistent session. The `user_id` stamped on a team session's row records the member who opened it first; it is actor provenance, and it grants that member nothing. One narrow exception to the no-org-fallback rule above: an org admin holds the same owner-level authority on a team session. This is the recovery path the team mutation routes already give them, so a team whose last admin left the org is never stranded.
- **Eligibility is re-checked at action time**, not delivery time: a decision-gate resolution or prompt from a forwarded card is validated against *current* membership at click. Removal from a team breaks the member's sourced credentials and evicts them from live team-session connections immediately.
- **User orchestrators** are visible and steerable only by their owner. **Org orchestrators** are readable by members, steerable by admins.
- Decision gates route to actors authorized to resolve them: child-session gates to the parent's audience (team members for team-owned parents, the owner for personal), org-orchestrator gates to admins or an automation rule's designated approvers.
Expand Down
Loading