docs(personas): accuracy sweep — spec models, protocol contracts, page corrections

Spec models now describe what the endpoints do: ListPersonasResponse
declares the tool_inventory the shelf depends on, both console create
models declare persona, CreatePersonaRequest declares org_id, and
UpdatePersonaRequest documents the null-vs-absent split (null clears
base_prompt/tool_allowlist, null on flags/kinds is ignored). Console
OpenAPI regenerated.

Protocol contracts match the implementations: update_persona's return
covers the no-op case, create_persona's raises-list is complete, and
both extended row-shape docstrings gain their tail columns plus the
append-only rule. The workstreams.persona comments say slug, not
display name.

Page corrections from the docs review: personas.md documents the
creative_mode-to-writer migration conversion, the mid-session /resume
MCP-lever behavior, visibility-based nudge gating, the soft-set
prompt-cache cost, and the executive tool list — and drops internal
jargon. The changelog entry moves under [Unreleased] with the house
breaking-marker style and the auto-conversion note. coordinator-skills
and the API tour stop using persona to mean framing; governance,
api-reference, sdk, console, tools, and memory pick up the new
permission family, endpoints, kwargs, picker, and lever caveats.
This commit is contained in:
Patrick Buckley
2026-07-02 08:12:30 -07:00
parent 09c05733c6
commit b65e5cae0e
14 changed files with 177 additions and 44 deletions
+10 -4
View File
@@ -13,9 +13,9 @@ stable, and the experimental line:
- **`stable/1.6`** — patch-only (`v1.6.x`)
- **`main`** — experimental (next major)
## [Unreleased — 1.7]
## [Unreleased]
### Personas
### Added
- **Personas** (#683) — a named, reusable bundle attached to a workstream
at creation, controlling system-message composition and the capability
@@ -31,13 +31,19 @@ stable, and the experimental line:
`turnstone --persona <name>`); authored in the console's new
Governance → Personas tab (`persona.{create,read,write}` perms,
archive-only lifecycle). See `docs/personas.md`.
- **BREAKING: `/creative` removed.** The REPL toggle (and its tab
### Removed
- **`/creative` removed** *(BREAKING)* — the REPL toggle (and its tab
completion) is gone; the `writer` seed persona replaces it — start a
session with `turnstone --persona writer` or pick *Writer* in the web
pickers. Unlike the old fork, the writer persona composes the full
system message, so session context and mandatory prompt policies now
apply to prose-only sessions too. The `creative_mode` key in
`workstream_config` is no longer read or written.
`workstream_config` is no longer read or written. Migration `063`
converts existing creative-mode workstreams to the `writer` persona
automatically, so they resume as writing sessions rather than as
legacy defaults.
## [1.6.0]
+37
View File
@@ -698,6 +698,42 @@ Each skill summary:
---
### `GET /v1/api/personas`
Returns the enabled personas offered by the workstream-creation pickers.
Authenticated for any logged-in user and deliberately gated by **no**
`persona.*` permission — selecting a persona at creation is a user
action, while the `persona.*` perms gate authoring. Display fields only;
the levers (base prompt, tool set, MCP/memory toggles) stay server-side.
**Response:**
```json
{
"personas": [
{"name": "engineer", "display_name": "Engineer", "description": "The stock interactive workstream: full tools, MCP, and memory.", "applies_to_kinds": ["interactive"], "is_default": true},
{"name": "researcher", "display_name": "Researcher", "description": "Answers questions with evidence, read-only. Never modifies anything.", "applies_to_kinds": ["interactive"], "is_default": false}
],
"total": 2
}
```
Each persona summary:
| Field | Type | Description |
|--------------------|--------|------------------------------------------------------------------|
| `name` | string | Persona slug (used in the `persona` field on workstream creation) |
| `display_name` | string | Human-readable label for pickers |
| `description` | string | Short description of the persona's intent |
| `applies_to_kinds` | array | Workstream kinds the persona applies to (`interactive` / `coordinator`) |
| `is_default` | bool | Whether this is the default persona for its kind |
> **Note:** For full persona management (create, edit, archive), use the
> admin endpoints at `/v1/api/admin/personas` (requires the
> `persona.{create,read,write}` permissions).
---
### `POST /v1/api/workstreams/{ws_id}/send`
Sends a user message to a workstream. Spawns a daemon worker thread that calls
@@ -895,6 +931,7 @@ All fields are optional. The body can be empty or an empty JSON object.
| `auto_approve` | bool | false | Auto-approve all tool calls for this workstream |
| `resume_ws` | string | "" | Workstream ID to resume atomically during creation (empty = fresh)|
| `skill` | string | "" | Skill name. Applies content (system prompt), model, temperature, reasoning effort, max tokens, auto-approve policy, token budget, and other session config from the skill. Returns 400 if not found or disabled. Ignored when `resume_ws` is set (resumed sessions restore their own skill). |
| `persona` | string | "" | Persona slug. Resolved and snapshotted into the workstream at creation; empty selects the kind's default. |
| `judge_model` | string | "" | Optional model alias for the judge (overrides default judge model for this workstream) |
> **Skill behavior:** When `skill` is specified, the skill's content is injected as a system message and its session config fields (model, temperature, auto-approve, token budget, etc.) override system defaults for the new workstream.
+4 -3
View File
@@ -379,6 +379,7 @@ Breadcrumb: `Cluster > Running` or `Cluster > db-west-04`. Server-side paginated
Triggered by the "+ new" header button. A modal dialog with:
- **Node selector** — dropdown with three targeting modes: "Auto (best available)" picks the node with the most headroom, "General pool (any node)" picks a node with available capacity using round-robin, or a specific node from the list (showing capacity).
- **Persona** — optional dropdown listing the enabled personas for the workstream kind. Sets the system-message composition and capability envelope at creation, snapshotted server-side; empty uses the kind's default. Picking one requires no `persona.*` permission.
- **Profile** — optional dropdown listing enabled skills. Applies the skill's model, auto-approve policy, token budget, and other behavioral settings at creation time.
- **Name** — optional text input. Auto-generated if left empty.
- **Model** — optional text input for a model alias from the target node's registry.
@@ -396,9 +397,9 @@ The browser maintains a local `clusterState` object that mirrors the cluster sna
Accessed via the "admin" button in the header (visible when authenticated
with `approve` scope). Provides user, API token, channel link, MCP server,
and skill management with 18 tabs (Users, API Tokens, Channels, Schedules,
Watches, Roles, Policies, Prompts, Judge, Skills, MCP Servers, Usage,
Audit, Memories, Models, Nodes, Settings, TLS). See also
and skill management with tabs that include Users, API Tokens, Channels,
Schedules, Watches, Personas, Roles, Policies, Prompts, Judge, Skills,
MCP Servers, Usage, Audit, Memories, Models, Nodes, Settings, and TLS. See also
[Governance](governance.md) for the Roles, Policies, Skills, Usage, and
Audit tabs, and [Settings](settings.md) for the database-backed
configuration editor.
+1 -1
View File
@@ -366,7 +366,7 @@ deleted.
- [coordinator-skills.md](coordinator-skills.md) — writing a skill
that runs on a coordinator session (orchestrator framing,
workflow patterns, `SkillKind` classifier).
workflow patterns, `SkillKind` classifier).
- [bulk-endpoints.md](bulk-endpoints.md) — the two bulk-shape
idioms (`{results, denied, truncated}` vs
`{<bucket>, failed, skipped}`) used by `cluster/ws/live`,
+8 -8
View File
@@ -1,13 +1,13 @@
# Writing a coordinator-specific skill
Skills are prompt-level personas that steer a Turnstone session
A skill is prompt-level framing that steers a Turnstone session
toward a narrow task. Most skills target **interactive** sessions —
the single-workstream "do this thing" surface where the model wields
`bash`, `edit_file`, `web_fetch`, and the rest of the maker toolset.
A **coordinator skill** is different. It runs on a session whose job
is to orchestrate other sessions. The toolset is smaller and
narrower, the persona is an orchestrator instead of a maker, and the
narrower, the role is an orchestrator instead of a maker, and the
success metric is "did the plan resolve" instead of "did the code
compile". This doc covers the differences a skill author has to
care about.
@@ -22,8 +22,8 @@ migration 044 added the column). Three values:
| `SkillKind` enum | Stored as | Meaning |
|-------------------------|-----------------|----------------------------------------------------------------------------|
| `SkillKind.INTERACTIVE` | `"interactive"` | Authored for the interactive maker persona (single-workstream "do this"). |
| `SkillKind.COORDINATOR` | `"coordinator"` | Authored for the orchestrator persona (delegate, monitor, synthesise). |
| `SkillKind.INTERACTIVE` | `"interactive"` | Authored for the interactive maker role (single-workstream "do this"). |
| `SkillKind.COORDINATOR` | `"coordinator"` | Authored for the orchestrator role (delegate, monitor, synthesise). |
| `SkillKind.ANY` | `"any"` | Either surface (or audience-neutral). Default on create. |
The `kind` field is a `StrEnum` — drop-in `str` compatible — so DB
@@ -96,15 +96,15 @@ for the output. The coordinator stays the orchestrator.
---
## Persona differences
## Framing differences
Interactive skills compose on top of `base_interactive.md` — a
"maker" persona: get the work done, use the tools, edit the code,
"maker" framing: get the work done, use the tools, edit the code,
close the loop.
Coordinator skills compose on top of
[`base_coordinator.md`](../turnstone/prompts/base_coordinator.md) —
an "orchestrator" persona: decompose, delegate, monitor, synthesise.
an "orchestrator" framing: decompose, delegate, monitor, synthesise.
The base text is short but sets the tone every coordinator skill
inherits:
@@ -339,7 +339,7 @@ For a new coordinator skill:
A full end-to-end test isn't required for every skill; a
prepare-step unit test that asserts "given this initial message, the
first tool call is X with Y args" is usually sufficient to catch
persona drift without a real LLM in the loop.
framing drift without a real LLM in the loop.
---
+8 -3
View File
@@ -13,7 +13,7 @@ The permission model has two layers:
1. **Scopes** (legacy) — `read`, `write`, `approve`. Checked by `AuthMiddleware`
on every request based on URL path classification.
2. **Permissions** (granular) — 15 permission strings checked per-endpoint by
2. **Permissions** (granular) — named permission strings checked per-endpoint by
`require_permission()`.
**Built-in roles** (seeded by migration 008):
@@ -24,7 +24,11 @@ The permission model has two layers:
| operator | read, write, workstreams.create, workstreams.close |
| viewer | read |
Custom roles can be created with any subset of the 15 valid permissions.
Custom roles can be created with any subset of the valid permissions.
The `persona.create` / `persona.read` / `persona.write` family gates
persona administration; migration `063` seeds all three onto
`builtin-admin`, and any role can be granted them through the standard
role and permission-override editors.
**Auth flow:**
1. User logs in (password or API token) → `_load_user_permissions()` aggregates
@@ -177,6 +181,7 @@ All under `/v1/api/admin/` (requires `approve` scope + granular permission).
| Orgs | 3 (list, get, update) | `admin.orgs` |
| Tool Policies | 4 (CRUD) | `admin.policies` |
| Skills | 4 (CRUD) | `admin.skills` |
| Personas | 4 (list, create, get, edit/archive) | `persona.read` / `persona.create` / `persona.write` |
| Schedules | 6 (CRUD + runs) | `admin.schedules` |
| Watches | 3 (list, create, cancel) | `admin.watches` |
| Usage | 1 (aggregated query) | `admin.usage` |
@@ -222,7 +227,7 @@ Both Python and TypeScript console SDKs expose governance methods:
- **Privilege escalation prevented**: `admin_assign_role` blocks self-assignment
and requires caller to hold a superset of the target role's permissions
- **Permission validation**: Role create/update validates permissions against
a 15-item allowlist (`_VALID_PERMISSIONS`)
the permission allowlist (`_VALID_PERMISSIONS`)
- **Self-deletion blocked**: `admin_delete_user` rejects attempts to delete
your own account (matching the self-assignment guard on role endpoints)
- **Field allowlists**: Storage `update_*` methods filter fields against
+5
View File
@@ -75,6 +75,11 @@ This means the model always has its most relevant memories available without
explicit recall -- but can still use `memory(action='search')` for deeper
lookup.
The persona memory lever gates this pathway: a workstream whose persona
turns memory off receives no relevance injection at all -- the steps
above run only when memory is enabled for the session. See
[Personas](personas.md).
### Nudges
The metacognition layer can nudge the model to save memories at appropriate
+16 -6
View File
@@ -12,9 +12,9 @@ A persona is exactly four levers — no more:
| Lever | What it does |
|---|---|
| **Base prompt** | Replaces the BASE module of the composed system message (`base.md` / `base_coordinator.md`). *Only* BASE: ENV, CONTEXT, TOOLS, and POLICIES keep composing, so mandatory [prompt policies](governance.md) ride on top of every persona. Empty = the kind's stock base. |
| **Tool visibility** | Which tools the session advertises. Tri-state: *unrestricted* (tracks tool growth and MCP catalogs), *no tools* (the TOOLS prompt block self-suppresses and zero definitions go on the wire), or an *exact set* of names. Including `tool_search` in a set makes it **soft** — tools the model discovers through search join the visible set; omitting it makes the set **hard** (the search pathway is disabled entirely). |
| **Tool visibility** | Which tools the session advertises. Tri-state: *unrestricted* (tracks tool growth and MCP catalogs), *no tools* (the TOOLS prompt block self-suppresses and zero definitions go on the wire), or an *exact set* of names. Including `tool_search` in a set makes it **soft** — tools the model discovers through search join the visible set; omitting it makes the set **hard** (the search pathway is disabled entirely). On commercial providers a soft set costs one prompt-cache re-prime per `tool_search` expansion, since each expansion rewrites the wire tool set and recomposes the prompt. |
| **MCP** | Whether the workstream talks to MCP at all. **Session-wide**: off means no MCP tools for the persona's own hands *or* for in-process task agents, no resource/prompt catalogs, and no listener registrations. This lever expresses infrastructure intent, not behavior shaping. |
| **Memory** | Whether the persona's **own hands** get memory: recalled-memory injection into the prompt, memory-directed metacognitive nudges, and the `memory` tool. Task agents keep their own envelope, and compaction spill/markers are session mechanics that are never persona-gated. |
| **Memory** | Whether the persona's **own hands** get memory: recalled-memory injection into the prompt, memory-directed metacognitive nudges, and the `memory` tool. Task agents keep their own envelope, and compaction spill/markers are session mechanics that are never persona-gated. An exact tool set that hides `memory` also mutes those nudges, and the compaction-resume pointer follows `recall`'s visibility. |
Visibility is behavior shaping, **not** a security boundary: any tool call
that does reach the wire still clears the same approval, judge, and policy
@@ -29,6 +29,12 @@ reads only the stamp:
- **Editing or archiving a persona never changes an existing workstream.**
Rehydrate, resume, and post-compaction resume all run from the stamp.
A mid-session REPL `/resume` adopts the target workstream's stamp for
prompt, tools, and memory; for the MCP lever it can only narrow in
place — adopting an MCP-off stamp drops the live MCP surface, while
adopting an MCP-on stamp into a session whose persona dropped MCP at
construction is refused with an error telling you to reopen the
workstream fresh.
- A workstream outlives its persona — an archived persona keeps labelling
the workstreams stamped with it.
- A partial or unparseable stamp is treated as corruption: session
@@ -36,7 +42,10 @@ reads only the stamp:
envelope the operator never chose.
- Workstreams created before personas existed carry no stamp and keep
legacy behavior, byte-identical to the `engineer` / `orchestrator`
defaults below.
defaults below — with one exception: pre-1.7 workstreams that had
`creative_mode` set are converted by migration `063` into full
`writer` stamps, so they resume as writing sessions rather than as
legacy defaults.
- Forking (`resume_ws` on create) resumes the source's stamped persona; the
fork does not re-resolve.
@@ -53,7 +62,7 @@ personas existed:
| `scribe` | interactive | custom (faithful structuring of given material) | none | off | off |
| `researcher` | interactive | custom (evidence-first, read-only) | `read_file`, `search`, `web_fetch`, `web_search`, `recall`, `memory` (hard) | off | on |
| `writer` | interactive | custom (creative writing partner — replaces the removed `/creative`) | none | off | on |
| `executive` | coordinator | custom (delegate, interrogate plans, judge outcomes) | 10 delegation/inspection tools (hard) | off | on |
| `executive` | coordinator | custom (delegate, interrogate plans, judge outcomes) | spawn/inspect/lifecycle tools plus `memory`: `spawn_workstream`, `spawn_batch`, `send_to_workstream`, `wait_for_workstream`, `inspect_workstream`, `list_workstreams`, `list_nodes`, `close_workstream`, `cancel_workstream`, `memory` (hard) | off | on |
Notes:
@@ -82,7 +91,8 @@ seeded):
startup. `--resume` ignores `--persona` and adopts the resumed
workstream's stamp.
- **Coordinator spawn**: `spawn_workstream` / `spawn_batch` take a
`persona` argument, validated at prep time (children are always
`persona` argument, validated when the coordinator prepares the spawn
and re-checked by the node that creates the child (children are always
interactive-kind). Omitted means the interactive **default** — a child
never inherits its parent coordinator's persona. Sub-agents spawned via
`task_agent` have no persona parameter at all; they keep their own
@@ -91,7 +101,7 @@ seeded):
## Authoring (console)
Personas are managed in the console's **Manage → Governance → Personas**
tab. The Service Hatch shelf exposes exactly the four levers plus the kind
tab. The admin shelf exposes exactly the four levers plus the kind
list, the default marker, and archive. Rules:
- `name` is an immutable lowercase slug; edit `display_name` instead.
+2 -2
View File
@@ -69,7 +69,7 @@ Both `TurnstoneServer` (sync) and `AsyncTurnstoneServer` (async) expose:
|----------|--------|---------|
| **Workstreams** | `list_workstreams()` | `ListWorkstreamsResponse` |
| | `dashboard()` | `DashboardResponse` |
| | `create_workstream(*, name, model, auto_approve, skill, initial_message, attachments)` | `CreateWorkstreamResponse` |
| | `create_workstream(*, name, model, auto_approve, skill, persona, initial_message, attachments)` | `CreateWorkstreamResponse` |
| | `close_workstream(ws_id)` | `StatusResponse` |
| **Attachments** | `upload_attachment(ws_id, filename, data, *, mime_type=...)` | `UploadAttachmentResponse` |
| | `list_attachments(ws_id)` | `ListAttachmentsResponse` |
@@ -100,7 +100,7 @@ Both `TurnstoneConsole` (sync) and `AsyncTurnstoneConsole` (async) expose:
| | `workstreams(*, state, node, search, sort, page, per_page)` | `ClusterWorkstreamsResponse` |
| | `node_detail(node_id)` | `NodeDetailResponse` |
| | `snapshot()` | `ClusterSnapshotResponse` |
| | `create_workstream(*, node_id, name, model, initial_message, skill)` | `ConsoleCreateWsResponse` |
| | `create_workstream(*, node_id, name, model, initial_message, skill, persona)` | `ConsoleCreateWsResponse` |
| **Schedules** | `list_schedules()` | `ListSchedulesResponse` |
| | `create_schedule(*, name, schedule_type, initial_message, ...)` | `ScheduleInfo` |
| | `get_schedule(task_id)` | `ScheduleInfo` |
+5
View File
@@ -580,6 +580,11 @@ Tool search uses the best available mechanism for each provider:
`_exec_tool_search()` runs a pure-Python BM25 index over tool names and
descriptions, then expands the matched tools into the visible set.
A persona with a tool-visibility set overrides this selection: any exact
set forces tool search into the client-side BM25 mechanism (tier 3)
regardless of provider, and a **hard** set — one whose visible tools omit
`tool_search` — disables tool search entirely.
### Configuration
Tool search is configured in `config.toml` under the `[tools]` section:
+30 -1
View File
@@ -7785,6 +7785,12 @@
"title": "Skill",
"type": "string"
},
"persona": {
"default": "",
"description": "Persona slug; resolved and snapshotted at creation, empty = kind default",
"title": "Persona",
"type": "string"
},
"resume_ws": {
"default": "",
"description": "Workstream ID to resume (loads previous conversation)",
@@ -8112,6 +8118,12 @@
"description": "Optional skill name to apply to the coordinator session.",
"title": "Skill"
},
"persona": {
"default": "",
"description": "Persona slug; resolved and snapshotted at creation, empty = kind default",
"title": "Persona",
"type": "string"
},
"initial_message": {
"default": "",
"description": "Optional first user message dispatched to the new coordinator session.",
@@ -11232,6 +11244,12 @@
"default": true,
"title": "Enabled",
"type": "boolean"
},
"org_id": {
"default": "",
"description": "Owning org (informational; capped at 64)",
"title": "Org Id",
"type": "string"
}
},
"required": [
@@ -11241,7 +11259,7 @@
"type": "object"
},
"UpdatePersonaRequest": {
"description": "PATCH body \u2014 only fields present in the JSON are applied.\n\nArchive = ``{\"enabled\": false}``; default flip = ``{\"is_default\": true}``\non the successor (storage demotes the incumbent atomically). ``name``\nis immutable; existing workstreams are never affected by edits.",
"description": "PATCH body \u2014 absent fields are left unchanged.\n\nExplicit ``null`` is meaningful only on the two resettable fields:\n``base_prompt: null`` clears the override back to the kind's stock\nBASE, and ``tool_allowlist: null`` resets to unrestricted. ``null``\non the boolean flags or ``applies_to_kinds`` is ignored (treated as\nabsent), so a client serializing unset optionals as null cannot\narchive a persona or flip levers by accident.\n\nArchive = ``{\"enabled\": false}``; default flip = ``{\"is_default\": true}``\non the successor (storage demotes the incumbent atomically). ``name``\nis immutable; existing workstreams are never affected by edits.",
"properties": {
"display_name": {
"anyOf": [
@@ -11369,6 +11387,17 @@
},
"title": "Personas",
"type": "array"
},
"tool_inventory": {
"additionalProperties": {
"items": {
"type": "string"
},
"type": "array"
},
"description": "Per-kind builtin tool names (plus the synthetic 'tool_search') for the visibility checklist \u2014 derived server-side so clients never hand-mirror the inventory",
"title": "Tool Inventory",
"type": "object"
}
},
"required": [
+25 -1
View File
@@ -147,6 +147,10 @@ class ConsoleCreateWsRequest(BaseModel):
default="", description="Optional first message sent after creation"
)
skill: str = Field(default="", description="Skill name (replaces default skills)")
persona: str = Field(
default="",
description="Persona slug; resolved and snapshotted at creation, empty = kind default",
)
resume_ws: str = Field(
default="", description="Workstream ID to resume (loads previous conversation)"
)
@@ -1085,10 +1089,18 @@ class CreatePersonaRequest(BaseModel):
applies_to_kinds: list[str] = Field(default_factory=lambda: ["interactive"])
is_default: bool = False
enabled: bool = True
org_id: str = Field(default="", description="Owning org (informational; capped at 64)")
class UpdatePersonaRequest(BaseModel):
"""PATCH body — only fields present in the JSON are applied.
"""PATCH body — absent fields are left unchanged.
Explicit ``null`` is meaningful only on the two resettable fields:
``base_prompt: null`` clears the override back to the kind's stock
BASE, and ``tool_allowlist: null`` resets to unrestricted. ``null``
on the boolean flags or ``applies_to_kinds`` is ignored (treated as
absent), so a client serializing unset optionals as null cannot
archive a persona or flip levers by accident.
Archive = ``{"enabled": false}``; default flip = ``{"is_default": true}``
on the successor (storage demotes the incumbent atomically). ``name``
@@ -1108,6 +1120,14 @@ class UpdatePersonaRequest(BaseModel):
class ListPersonasResponse(BaseModel):
personas: list[PersonaInfo]
tool_inventory: dict[str, list[str]] = Field(
default_factory=dict,
description=(
"Per-kind builtin tool names (plus the synthetic 'tool_search') "
"for the visibility checklist — derived server-side so clients "
"never hand-mirror the inventory"
),
)
class ModelReloadResponse(BaseModel):
@@ -1243,6 +1263,10 @@ class CoordinatorCreateRequest(BaseModel):
default=None,
description="Optional skill name to apply to the coordinator session.",
)
persona: str = Field(
default="",
description="Persona slug; resolved and snapshotted at creation, empty = kind default",
)
initial_message: str = Field(
default="",
description="Optional first user message dispatched to the new coordinator session.",
+21 -11
View File
@@ -413,12 +413,15 @@ class StorageBackend(Protocol):
Returns rows of ``(ws_id, alias, title, name, created, updated,
message_count, node_id, state, kind, model_alias, launch_skill,
child_count, context_tokens, context_window)`` ordered by updated
DESC. The trailing enrichment columns feed the saved-list DTO:
``model_alias`` / ``launch_skill`` come from ``workstream_config``;
``context_tokens`` is the most recent ``usage_events`` prompt size
and ``context_window`` the model's window (the caller divides them
for the occupancy ratio); ``child_count`` counts child workstreams.
child_count, context_tokens, context_window, project_id, user_id,
persona)`` ordered by updated DESC. The trailing enrichment columns
feed the saved-list DTO: ``model_alias`` / ``launch_skill`` come
from ``workstream_config``; ``context_tokens`` is the most recent
``usage_events`` prompt size and ``context_window`` the model's
window (the caller divides them for the occupancy ratio);
``child_count`` counts child workstreams. New columns MUST keep
appending at the tail — a full-arity unpack in session_routes
consumes this exact tuple.
"""
...
@@ -765,9 +768,10 @@ class StorageBackend(Protocol):
Returns a list of SQLAlchemy ``Row`` objects. **Prefer dict access
via ``row._mapping[<col>]``**; positional indexing is brittle against
future SELECT reorders and against new columns appearing in the
tail (the select currently ends with ``user_id, title, alias`` —
``title``/``alias`` were appended after ``user_id`` so existing
positional fallbacks that index up to row[9] stay valid).
tail (the select currently ends with ``user_id, title, alias,
project_id, persona`` — appended in that order, so positional
fallbacks that index up to row[9] stay valid; new columns MUST
keep appending at the tail).
"""
...
@@ -2421,11 +2425,17 @@ class StorageBackend(Protocol):
def create_persona(self, persona: dict[str, Any]) -> None:
"""Create a persona. Requires ``persona_id`` and ``name``; accepts the
Python-typed dict shape above (JSON serialization is internal).
Raises ValueError on duplicate name or invalid ``applies_to_kinds``."""
Raises ValueError on: missing ``persona_id``/``name``, duplicate
name, invalid ``applies_to_kinds``, oversized fields (caps live in
``_utils.serialize_persona_fields``), or an ``is_default`` persona
that is multi-kind or disabled."""
...
def update_persona(self, persona_id: str, **fields: Any) -> bool:
"""Update PERSONA_MUTABLE fields. Returns True if the persona exists.
"""Update PERSONA_MUTABLE fields. Returns True only when the persona
exists AND at least one mutable field was supplied — a no-op call on
a real row returns False (check existence separately if you need to
distinguish "not found" from "nothing to update").
Invariants (raise ValueError): a default persona cannot be archived,
cannot drop its ``is_default`` flag directly (flip the flag on the
+5 -4
View File
@@ -109,10 +109,11 @@ workstreams = sa.Table(
# constraint (this schema family declares none — see migration 058).
# Added in migration 062.
sa.Column("project_id", sa.Text, nullable=True),
# persona: display name of the persona the workstream was created with
# (NULL = pre-persona workstream). Display/forensics only — the full
# persona snapshot lives in workstream_config; nothing reads this column
# to build a session. Added in migration 063.
# persona: SLUG of the persona the workstream was created with (NULL =
# pre-persona workstream) — personas.name, not display_name; clients
# resolve the display label. Display/forensics only — the full persona
# snapshot lives in workstream_config; nothing reads this column to
# build a session. Added in migration 063.
sa.Column("persona", sa.Text, nullable=True),
sa.Column("created", sa.Text, nullable=False),
sa.Column("updated", sa.Text, nullable=False),