# HTTP API
[中文](API.md) · [v3 detailed contract](PROMPT_API_V3_en.md) · [Frontend integration](FRONTEND_INTEGRATION_en.md)
[MVU state source](MVU_en.md)
Current assembly sources use shared `dshPromptSources` protocol 1; legacy `tavernRequestSources` aliases that registry. See the [complete API surface](API_SURFACES_en.md) for all extension routes, public imports and compatibility boundaries.
Request layout CRUD, application, preview, recorded request references and the optional advanced core extension are documented in [Request assembly](REQUEST_ASSEMBLY_en.md).
Contract: current Tavern source (package version in package.json), compatible with `>=0.2.1-rc.2 <0.2.2-0`, verified on rc.2 and rc.3.
Root: `/pmp-dsh-tavern/api`. API versions and DSH log format V4 are independent.
All endpoint catalogs use **Method / Path / Behavior / Status**, following the v2
format. Paths are relative to the stated version prefix. URL-encode identifiers;
query parameters and request bodies remain endpoint-specific. Existing local TCP
peer, Host, Origin and media-type checks apply. Except for raw export attachments, successful JSON
responses carry `ok:true`; failures carry `ok:false` and `error`. v1 error shapes and method rejection
codes vary by resource; common documentation formatting does not change wire contracts.
The [DSH V4 coordinate migration contract](DSH_0.1.7_MIGRATION_en.md) governs
message coordinates, branch inputs, and unmigrated timeline references.
## Impact on third-party consumers in the current version
The current contract targets DSH `0.2.1-rc.2` / `0.2.1-rc.3`, including the optional Host services and session opening world-book routes below. HTTP message responses retain source text; third-party clients choose their own rendering. Persistent operation logs, the desktop request token, avatars and presentation settings retain their existing contracts.
Compared with `v2.3.2`, the API root, v1/v2/v3 prefixes and existing routes remain.
This does not mean that every accepted input, historical reference or Host integration behavior is unchanged.
The runtime range is `>=0.2.1-rc.2 <0.2.2-0`. Later 0.2.1 RCs and the final release are admitted by policy; reassess 0.2.2 and its prereleases.
| Integration | Impact and consumer requirements |
| --- | --- |
| Ordinary resource and binding operations | Existing valid requests keep their routes. User/template import/export and explicit template creation are additive and need not be adopted by existing clients |
| Template creation and updates | Existing `{ name, sourceSessionId }` creation remains. POST/PATCH now reject unknown top-level fields: do not send back a full response containing IDs or timestamps. `selection` and `sourceSessionId` are mutually exclusive; explicit `selection` replaces the whole configuration rather than merging a partial patch |
| Historical messages, timelines and branches | Current `sessionFormatVersion` is 4. Read, store and submit coordinates with their source format version. Missing or mismatched V4 reference versions return `409 PLAY_COORDINATES_MIGRATION_REQUIRED`. Do not merely relabel old integers or assume a constant offset |
| Third-party stored references | The Tavern upgrade tool only processes the files listed in the migration guide; it does not scan third-party databases or caches. Invalidate old caches and reread upgraded data; external references that must be retained need separate migration backed by official log evidence |
| Preset parameters | `reasoningEffort` can contain additional or provider-specific values; do not hard-code the old enum. Omitting it in PATCH preserves the stored value; null clears the override. Explicitly unsupported Tavern sampling overrides may fall back, so stored preset values do not establish actual request parameters |
| Trace consumers | v3 can add optional `parameters` (requested/effective/fallbacks/attempt); v1 audit can add `sessionFormatVersion`. Older records may omit them. Tolerate additional fields, use actual parameter diagnostics and handle unavailable historical body/failure references after migration |
| Third-party frontends embedded in DSH | Tavern's `pmpDshTavernChrome` contract remains. Plugins calling DSH directly must replace `sessions.open` with `uiWorkspace.openSession`, inject that service, and adapt session directory and per-session lifecycle handling. Pure Tavern HTTP consumers are unaffected by those UI service signature changes |
See the endpoint contracts below, [coordinate migration](DSH_0.1.7_MIGRATION_en.md),
[parameter diagnostics](PROMPT_API_V3_en.md#preset-parameter-diagnostics) and
[frontend integration](FRONTEND_INTEGRATION_en.md).
## Version responsibilities and overlap audit
The following defines the current responsibilities of v1, v2, and v3. v3 does not
provide a current-resource aggregator.
| Capability | Existing v1 coverage | Existing v3 coverage | Scope conclusion |
| --- | --- | --- | --- |
| Complete current resource fields | `/presets/:id`, `/characters/:id`, `/users/:id`, `/world-books/:id` | No current-resource aggregate | Resource reads belong to v1 |
| Current bindings and greeting options | Selection endpoints, resource world-books links, configuration preview; `/active` includes a current summary | Past bindings inside historical records only | Current configuration belongs to v1; consumers derive effective greetings and deduplicated links |
| Current counts, aggregate revision and sampling suggestions | Resource fields can be counted; `/active.callConfig` maps sampling, but there is no identical aggregate snapshot revision | No current counts or aggregate revision; historical section/source metadata retains counts and hashes | Consumers compose these values; v1 is not field-for-field response equivalent |
| Historical bindings, resource summaries and lore decisions | Legacy `/traces?sessionId=` compatibility reads; new v1 audit and v3 share one schema 4 record | Selection/audit in assembly details and legacy adaptation | Intentional historical audit overlap; old route retained, new captures write one canonical record |
| Historical named sections, source metadata, order and actual system-message verification | None; `/active` reruns assembly using current state | Assembly index and detail; detail verifies and resolves bodies from official history | Independent v3 responsibility; `source.text` is not stored and current configuration cannot stand in for history |
Implemented boundary: **v1 owns current resources/configuration, v2 owns play
Session/workspace primitives, and v3 owns per-request assembly metadata,
official-history references, and provenance.** `/sessions/:id/sources` is not part
of the v3 contract: GET returns 404 with no alias or v1 redirect. Historical
`sections[].sources` retain past input relationships as metadata, hashes, and counts;
new records do not store `source.text`.
Historical overlap does not imply identical IDs or wire fields. New v1 audit and v3 assembly
metadata use the same audit in one canonical schema 4 record. Legacy schema 3 detail may predate
the final state of older v1 metadata; those files remain read-only compatibility inputs.
v1 `/active` runs loader assembly and lore matching. It does not create a new
historical Trace record and is not a cheap configuration-only GET. For current
configuration, use `POST /session-configurations/preview` with
`source: {mode:"current",sessionId}`, then read resources and linked books by ID.
Preview includes saved template-scoped RP fields; read runtime RP/pending state separately
from `/rp-mode`. This path runs no prompt assembly, but multiple HTTP reads are not
an atomic cross-resource snapshot.
DSH history and v2 `/sessions/:id/messages` provide authoritative message reads.
They do not guarantee original pre-assembly fields, disabled content or Tavern
field provenance, and cannot reconstruct complete resources or historical source bodies.
The v3 detail route verifies official references before returning section/context bodies;
source text remains unavailable. Official `system-prompt/assemble` offers runtime
observation, adjustment, and contribution independently of HTTP v3.
Failures are not extra assistant messages; v2 does not add failure placeholders. v3 can read
`failure {code,message}` through verified official failure-event references, with explicit
unavailability on missing or invalid history. See [failure details](PROMPT_API_V3_en.md#failure-details).
Audit evidence: [Trace API handler](../packages/tavern-loader/src/prompt-trace-api.js),
[configuration preview](../packages/session-template/src/service.js),
[v1 preset/active routes](../packages/preset/src/server.js),
[historical recorder](../packages/tavern-trace/src/assembly-recorder.js),
[Trace client](../packages/tavern-trace/src/client.js).
For public contracts and read timing, see [consumer read paths and compatibility boundaries](PROMPT_API_V3_en.md#consumer-read-paths-and-compatibility-boundaries).
An assembled section is not necessarily an original source field; current resources, historical sections,
and runtime assembly are not interchangeable.
## Current session bound resources
Session resource views can call the optional Host method `tavernMemorySources.listBound({scope:{sessionId,authority:'local'},signal?})`, returning deeply immutable `{items,revision,checkCurrent}`. Items contain only `{id,adapterId,name,type,revision,managementMode,binding}` and optional `enabled/sourceError`, with no content or variables. Binding metadata identifies the real session, current character, preset/Persona origins or existing MVU instance identity. The directory covers the current character's embedded book, effectively selected standalone and session opening books, templates actually bound to the session, and existing current-character MVU state instances. Inline preset prompts are not represented as persistent resources.
The lookup checks selected books without scanning other cards/global resources, activating entries, allocating MVU instances or granting write/prompt usage. Global `list/read` remain available for global resource management. Session views must filter configuration and historical traces through these actual IDs; unbound configuration or old traces must not recreate other-card rows. Cross-card binding UI is outside this interface. After the final await, consumers check the Host-only `checkCurrent()` lease, revoked by selection generations, source versions, session instance/identity changes and service unload. An absent MVU service contributes no items; an older service lacking metadata support explicitly rejects rather than guessing binding through content catalogs. World-book/template adapters and `tavernMvu` also expose this same `listBound` shape for direct manager forwarding. No HTTP route is added.
## Session opening world books
The optional Host service `tavernOpeningWorldBooks` exposes `prepare` and `commit`; types are exported by `pmp-dsh-tavern/opening-worldbook`. HTTP uses `POST /pmp-dsh-tavern/api/v1/sessions/:sessionId/opening-worldbook/prepare` and `/commit`, retaining local peer, Host, Origin/desktop token, JSON and official DSH admission checks.
`prepare({sourceIdentity,openingId,identitySource,source?})` reads data and returns `{ok:true,proposalId,expectedRevision,entriesHash,entries,openingId,entryCount,expiresAt,sourceIdentity,resourceId}`. `sourceIdentity` is `{version:1,owner:'pmp-dsh-tavern',sessionId,characterId,greetingIndex,greetingSha256,identitySha256}`: the index is zero based, 0 is first_mes, and greetingSha256 covers the original before regex/macros. The Host verifies the official session, selected original card and selection generation. `identitySource` and `source:{url,sha256,content}` accept only the fixed public snapshots in `opening-worldbook/manifest`; their scripts are never executed. Either sibling contains both full families. default/alisa_party skip, while police_done/hospital_done/pool contain 7/8/15 entries. Bounded literals and fixed data mappings preserve full content; missing data, unsupported expressions or hash mismatches reject.
After showing the proposal and obtaining separate world-book write confirmation, the trusted parent UI calls `commit({proposalId,expectedRevision,operationId,sourceIdentity,reviewed:true,write:true})`. This does not reuse MVU variable grants. The guest VM can request a controlled opening ID and await a receipt; it receives no confirmation primitive, source reading, fetch or arbitrary Host calls. At most 64 proposals remain valid for 10 minutes. Source, selection ABA, session instance or unload changes revoke them. CAS precedes atomic persistence. Repeating a resource operationId returns its original receipt; conflicting reuse rejects. Empty skips create no resource, and their receipts can be retried only within the current Host lifetime.
Receipts are `{ok:true,inserted,existing,updated,targetWorldbook,method:'session-local',receiptId,resourceId,revision}`; empty choices also return `skipped:true` and `resourceId:null`. Only plugin-owned session storage changes. Original cards, global books and global bindings retain their values. Resource IDs are `world-book:session-opening-`, readable/listable under the world-book `tavernMemorySources` adapter only within their session scope. A matching current selection uses existing keyword, enabled, probability, budget and placement activation. Actual ownership follows the current trusted manager registration. While registered, source defaults and explicit retrieve overrides apply; missing decisions and denies suppress assembly. Removal restores source defaults. A commit receipt grants no prompt usage and establishes no model delivery or provider receipt.
## Optional Host scope catalog
The loader provides `tavernScopeCatalog` (`protocolVersion:1`, `authority:'local'`), exported through `pmp-dsh-tavern/scope-catalog`. Consumers use optional `ctx.inject`; a missing service or incompatible version is explicitly unavailable. A full resource listing is not a directory fallback. This service exposes current resource identities without prompt assembly, prompt-use permission or resource writes.
`searchScopes({field,query?,cursor?,limit?,scope?:{sessionId?},signal?})` returns `{items:[{id,name}],nextCursor:string|null}`. Fields are `characterId`, `presetId` and `userId`; limit is 1–50 (default 50) and query is at most 200 characters, matched as a substring of ID/name. Global search omits sessionId. Results use stable ID order. Source-signed cursors bind field, normalized query, session boundary, catalog generation and visibility revision. Restart from the first page after changing the query or source. Every page rechecks source visibility and lifetime; bodies, avatars and filesystem paths are excluded.
Each source maintains a metadata index containing only ID, name and file fingerprints. Initialization/restart rebuilds metadata for new or changed resources; source-owned create/update/delete keeps it current. Paging and scope resolution read the index and filesystem metadata, without resource `list/get` or body reads. Direct external file edits return `SCOPE_CATALOG_STALE`; reloading the source rebuilds its index. An unavailable index does not disable the native resource store.
`resolveScopeContext({sessionId,signal?})` returns `{scope:{sessionId,characterId,presetId,userId},revision,checkCurrent}`. characterId maps `selection.characterCardId`; presetId denotes the ST prompt preset and userId denotes the Tavern RP persona. Missing/invisible resources resolve to null. The session must exist in the official `sessions` service. `checkCurrent` is a synchronous Host-only lease bound to the session instance, selection generation, catalogs and source visibility. Unload or any change revokes it. Rule consumers must recheck immediately before final use; browser-supplied identities cannot replace these facts.
Authority retains trusted local Host access, existing loopback HTTP protection and source visibility. This adds no principal ACL and does not interpret manager lists or directory reads as prompt-use permission. An optional consumer's HTTP routes must retain its own loopback protection.
## Desktop request token
`GET /pmp-dsh-tavern/api/request-token` requires `X-Tavern-Client: embedded` and returns `{ok:true,token}` with a process-local 64-character hex token and no-store caching. It uses the same TCP/Host checks, rejects foreign/null Origin and cross-site requests, and enables no CORS. The official `dsh-app://app` proxy strips Origin, so mutation requests from the embedded desktop client send `X-Tavern-Request-Token`. Only absent-Origin mutations may use this token; explicit foreign/null Origin remains forbidden. HTTP browser mutations keep same-origin validation. A Host restart invalidates tokens; the bundled client retries once after an origin rejection. This is CSRF protection, not authentication.
## v2 Tavern-owned interfaces
Tavern ownership means Tavern maintains interface semantics, not that DSH integration is version-independent. See the [operation log contract](OPERATION_LOGS_en.md) for stable surfaces, upgrade impact and event versions.
Prefix: `/pmp-dsh-tavern/api/v2`.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/operation-logs` | Filter/page bounded operation logs and export a JSONL page; see the persistent log contract below | Implemented |
| GET | `/chrome` | Returns `{ mode: "native" \| "play", revision }`. `revision` is the server-authoritative opaque version string | Implemented |
| GET | `/chrome/events` | Tavern-owned SSE. Sends the current snapshot on connect and `chrome/change` when mode actually changes | Implemented |
| PUT | `/chrome` | Writes global chrome. Does not change the RP lock or the current DSH session. Success includes the new `revision` (same mode produces no change) | Implemented |
| POST | `/chrome` | Not provided | 405 |
| GET | `/workspace` | Root path, whether a root is selected, contract version, warnings | Implemented |
| PUT | `/workspace` | Bind **one** existing play-workspace root. First selection may carry `SWIPE_DISK` / possible `SYSTEM_DISK` warnings. Does not mkdir the root | Implemented |
| POST | `/workspace/dirs` | `{ path }` relative path. `PlayWorkspaceStore` creates it inside the already-bound root jail. Compatible with native/browse Host; does not invoke the global directory picker or register a new DSH workspace. Unbound root → 409. Rejects absolute paths, `..`, symlink escape, and file conflicts | Implemented |
| GET | `/workspace/files?path=` | Read a UTF-8 file under the root. `catalog.json` / `timeline.json` run the matching schema/path checks after read. Third-party `ext` is kept as-is. Managed documents add a SHA-256 `revision` of the exact UTF-8 bytes (64 lowercase hex) | Implemented |
| PUT | `/workspace/files?path=` | Ordinary files still use `{ content, expectedWorkspaceScope? }`. `catalog.json` / `timeline.json` must send `expectedRevision`: `null` creates a missing target only; a 64-hex lowercase SHA-256 replaces only when the current byte hash matches. Validation, CAS, temp write, and rename share one target guard | Implemented |
| GET | `/workspace/files?list=` | List one prefix level | Implemented |
| POST | `/sessions` | Open a play session. With a character card, title = character name + time. Without a card, DSH `session.create` default title is used; no 409. Tavern bindings are copied only when the body has `selectionFromSessionId`. A root reply swipe may additionally send `stateSource:{sessionId,beforeReplyEventId}` with the same source session. Host freezes the original pre-turn MVU state and installs a separate instance before its first request. Ordinary new playthroughs omit it. Inserts into the play workspace. **Does not write timeline**. A cancelled request with no assistant body uses `stateSource:{sessionId,beforeUserEventId}` instead; the two coordinate fields are mutually exclusive. The source checks the durable aborted turn and freezes its pre-request checkpoint. | Implemented |
| POST | `/sessions/:id/branch` | `{ atEventId, sessionFormatVersion?, stateSource?:{sessionId,beforeReplyEventId} }`: log seq and its format version; migration checks below. Explicit reply-swipe stateSource requires this source session and a later target reply; history still forks at atEventId, while MVU uses the target pre-turn checkpoint. After fork, clear the child’s queued/steering input through public `sessionController.resolveAgent()` / `agent.inbox.clear()`, then copy the public selection. If the source import claim already ended at an earlier terminal, copy body-free pending lineage. Does not write timeline or send on behalf of the user. Queue cleanup or verification failure returns 502 `PLAY_BRANCH_INPUT_RESET_FAILED` before context copying. Copy failure is explicit 502 `PLAY_BRANCH_COPY_FAILED`. Open turn → 409 The same mutually exclusive `beforeUserEventId` coordinate supports retries of cancelled requests after the preserved prefix. | Implemented |
| POST | `/sessions/:id/user-message` | `{ text }` as the next user body, `session.prompt` `queue` | Implemented |
| GET | `/sessions/:id/messages` | `deriveMessages()` + `seq` + `incompleteTurn` + per-message `origin`; optional top-level `sessionFormatVersion` / `migratedFromV2`. Reads until `hasMore: false`; no plugin page cap. Empty Host cursor page, illegal seq, or a cursor that does not advance → 502 `PLAY_HISTORY_CURSOR_STALLED` | Implemented |
| GET | `/sessions/:id/coordinates` | Read current logical Session format and migration marker without message bodies. [Fields and examples](#session-coordinates) | Implemented |
| GET | `/sessions/:id/import-context` | Returns `{ binding }`. Unbound is `null`. A binding includes path/hash/state/count summaries and, when claimed, body-free claim identity/event-seq summaries. Record bodies are not returned | Implemented |
| PUT | `/sessions/:id/import-context` | `{ reference: { path, expectedHash? } }`. Bind or rebind an already-written workspace import-context on an empty session | Implemented |
| DELETE | `/sessions/:id/import-context` | Unbind an empty session. Idempotent `{ binding: null }` | Implemented |
| GET | `/playthroughs/:id/focus` | Resolve the playthrough via catalog and return `{ playthroughId, sessionId, nodeId, variantId }`. Empty playthroughs use `rootSessionId` | Implemented |
| GET | `/playthroughs/:id/availability` | Inspect every saved session reference of the specified run. Return status, reason, sessionIds, and workspaceScope without writes | Implemented |
| POST | `/playthroughs/:id/relink-character` | `{ characterId }`. Rebind only that playthrough and its root/swipe/branch descendant sessions to an existing card. An explicit user choice is not limited by automatic classification | Implemented |
| POST | `/playthroughs/:id/detach-session` | `{ sessionId }`. Remove the target session's timeline variant and descendants from that playthrough. Sibling branches, the DSH session/history, and the empty catalog playthrough remain. The server validates the tree and commits with managed-file revision/CAS | Implemented |
| GET | `/focus?path=` | Low-level compatibility route: derive `{ sessionId }` from an explicit timeline path. The bundled frontend uses the playthrough-id route | Compatibility surface |
| GET | `/focus` (no path) | No default target; “most recently written timeline” is not user focus | 400 `PLAY_FOCUS_PATH_REQUIRED` |
| POST | `/focus`, `/playthroughs/:id/focus` | Not provided | 405 |
The session messages response includes `stoppedRequest:null` or `{userEventId,turnStartEventId,turnEndEventId}` for the last durable aborted human request without assistant body text. Running turns, completed replies and source context do not qualify. It is evidence for a retry, not a saved reply variant.
Path exists but method is wrong → `405 PLAY_METHOD_NOT_ALLOWED` (for example `POST /chrome`, `POST /focus`, `GET /sessions`). On the stable focus path, a missing playthrough id is 404 `PLAY_PLAYTHROUGH_NOT_FOUND`; a missing catalog is 409 `PLAY_CATALOG_UNAVAILABLE`; a corrupt catalog stays 400 `PLAY_CATALOG_INVALID`; a missing or corrupt timeline is uniformly 409 `PLAY_FOCUS_UNAVAILABLE`. The stable entry does not accept a client path, does not read DSH history, and does not write files. Old `/focus?path=` remains migration compatibility only.
### Playthrough archive state
The optional playthrough field `ext.pmpDshTavern.archivedAt` is a UTC ISO timestamp in `YYYY-MM-DDTHH:mm:ss.sssZ` format; absence means active. Both the server and bundled client validate it. Archive and restore use the existing `PUT /workspace/files?path=catalog.json` revision/CAS contract: add or remove this field on the selected row while preserving other rows, extensions, names, numbers, paths, and session references. The bundled client rereads and replays local intent after conflicts; repeated archive retains the original timestamp. One-click archive composes the same catalog write. No dedicated archive or HTTP delete endpoint is provided.
Archive is list visibility, not detachment, session lifecycle, or disk cleanup. Timelines and selections remain unchanged; focus, history reads, and current turns remain usable. Sidebar ownership includes archived root, variant, head, and saved branch-head sessions so they do not become loose sessions; shared members remain visible in other active playthroughs. The archive box supports viewing and restoring. Everyday diagnostics exclude archived runs. Creation does not reuse an archived empty run but still counts its number. Older catalogs without the field behave as before; older clients ignore this visibility convention.
### One-click archive and cleanup inspection
`availability` returns `{ ok:true, id, status, reason?, sessionIds, workspaceScope }`. `status` is `invalid`, `available`, `empty`, `protected`, or `unavailable`. `invalid` requires at least one reference and confirmation through public `sessionController.inspect()` that every catalog root, timeline head, variant, and saved branch head is missing. Native archive, a different workspace, read failures, and corruption do not establish absence. Drafts, empty reference sets, and incomplete checks are preserved. Archived runs return `protected`.
One-click archive uses current-workspace diagnostics to include every run that cannot normally open, including missing or corrupt timelines, session read failures, or no available sessions. The client refreshes diagnostics, then writes archive markers for those exact run IDs and paths in one existing catalog revision/CAS update. Other rows are preserved. Healthy runs and healthy unstarted drafts stay visible. Unready session or workspace mirrors and workspace-level read failures prevent archive. The archive box restores runs without moving files or changing DSH sessions.
File writes optionally accept `expectedWorkspaceScope`, the JSON string for the workspace's `[workspaceId, rootPath]`. A non-string scope returns `400 PLAY_WORKSPACE_SCOPE_INVALID`. A different Host binding returns `409 PLAY_WORKSPACE_CHANGED`. Identical catalog content cannot replay the archive batch in another workspace.
The `availability` response above is the cleanup tool's strict absence check, not the one-click archive filter. Archive can collect corrupt or temporarily unavailable runs. Cleanup still requires confirmation that every reference is missing.
### Optional agent cleanup tool
Set `maintenanceTools: true` in the Tavern plugin configuration to register `tavern_cleanup_invalid_playthroughs`. It defaults to disabled and adds no required DSH service. Registration uses public `tools` and is disposed with the plugin. Card scripts cannot call this tool.
| Input | Behavior |
| --- | --- |
| `{}` or `{ "action":"preview" }` | Inspect the current workspace without writes, including archived runs. Return `workspaceScope`, run `items`, and recovery `backups` |
| `{ "action":"clean", "ids":["playthrough-example"], "workspaceScope":"" }` | Recheck selected runs, remove eligible catalog rows, and move each dedicated directory to `.tavern-trash//files` in the current workspace |
| `{ "action":"restore", "ids":[""], "workspaceScope":"" }` | Restore the original directory and merge its original row into the current catalog without overwriting existing directories or different rows with the same ID |
Mutations require the preview's workspace and 1 to 100 explicit target IDs. There is no implicit clean-all action. Results are per target. `cleaned` and `restored` mean completion; other states mean preserved, recovery required, or failed. `PLAY_CLEANUP_CONFLICT` means workspace, directory ownership, or recovery locations need a fresh preview or conflict resolution.
Cleanup accepts only runs whose sessions are all missing and whose timeline resides in a dedicated directory named after its `playthrough-*` ID. Shared directories, case aliases, symlinks, and files still referenced by other runs or import bindings are preserved. A durable `record.json` precedes catalog removal. Interrupted operations retain the original directory or recovery copy; restore with the backupId returned by execution or preview. Restoration retains the name, number, extensions, and archive state. Backups do not expire automatically or reclaim disk space. DSH sessions, history, selections, and global resources are never deleted.
### Query and use Session coordinate versions
The public read-only `GET /sessions/:id/coordinates` endpoint reports **the format governing the specified Session's current event sequence numbers**. It takes no body or query parameters; URL-encode the DSH Session ID. It returns no message bodies, log paths, storage schema, or old-to-new sequence map, and does not migrate Tavern data. External frontends can query it before reusing saved event references.
#### Request and fields
Call from a same-origin page that already has access to DSH Web. Existing Host access controls apply; there is no separate Tavern API key:
```js
async function queryCoordinates(sessionId) {
const response = await fetch(
`/pmp-dsh-tavern/api/v2/sessions/${encodeURIComponent(sessionId)}/coordinates`,
{ credentials: 'same-origin', headers: { Accept: 'application/json' } },
)
const data = await response.json()
if (!response.ok || data.ok !== true) {
throw Object.assign(new Error(data.error ?? `HTTP ${response.status}`), {
status: response.status, code: data.code,
})
}
return data
}
```
Example success (HTTP 200):
```json
{
"ok": true,
"sessionFormatVersion": 4,
"migratedFromV2": true
}
```
| Field | Type | Meaning and limits |
| --- | --- | --- |
| `ok` | boolean | `true` on success. |
| `sessionFormatVersion` | integer or null | The Session format version **defined by upstream DSH**, read from public `session.inspect()` metadata at `meta.version`. This is not the DSH software version, Tavern version, or HTTP `/v2` version. `0` is valid: do not use a truthiness check. `null` means the Host did not provide a usable version, not zero or compatible. |
| `migratedFromV2` | boolean | A **Tavern inference**: `true` when current events contain a synthetic V2→V3 system-message ID matching the upstream naming rule. V0/V1 restored through that edge can also return `true`. `false` only means no marker was detected; it does not prove the Session was never migrated or authoritatively report migration completion. |
The value describes the **logical Session format currently read by the Host**; it does not guarantee the original disk file has already been rewritten. Clients must use the returned value rather than infer it from software versions, and must not treat `>= 3` as a future-format compatibility promise. Tavern defines the API field names; DSH defines the format semantics and conversion rules.
If messages are already needed, use the top-level `sessionFormatVersion` and `migratedFromV2` from `GET /sessions/:id/messages` instead of making another request. Those message fields are optional, and older plugins may lack the standalone endpoint; missing/null means unknown. The endpoint avoids returning message bodies but still inspects the Session, so its cost is not guaranteed to be independent of history length.
#### Save, compare, and branch
1. When creating a QA range from messages, save the version from **that same response** in `variant.ext.pmpDshTavern.sessionFormatVersion`. For example: `startEventId: 9`, `endEventId: 16`, version `4`. Never query a version after an upgrade and use it to relabel old integers with unknown provenance.
2. Before reusing a saved range, query its variant's `sessionId` and compare versions for **equality**. A mismatch, unknown version, or unversioned reference with a migration marker requires verification/recovery before reuse. Absence of a marker does not establish that an old unversioned range is valid.
3. Send the saved range's version when branching. This conservative external-client example accepts only explicitly versioned, matching references; legacy unversioned data needs provenance verification first.
```js
async function branchSavedVariant(variant) {
const savedVersion = variant.ext?.pmpDshTavern?.sessionFormatVersion
const current = await queryCoordinates(variant.sessionId)
if (!Number.isSafeInteger(savedVersion) || savedVersion < 0
|| savedVersion !== current.sessionFormatVersion) {
throw new Error('Verify or migrate the saved event range before branching')
}
const response = await fetch(
`/pmp-dsh-tavern/api/v2/sessions/${encodeURIComponent(variant.sessionId)}/branch`,
{
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
atEventId: variant.endEventId,
sessionFormatVersion: savedVersion,
}),
},
)
const data = await response.json()
if (!response.ok || data.ok !== true) {
throw Object.assign(new Error(data.error ?? `HTTP ${response.status}`), {
status: response.status, code: data.code,
})
}
return data // HTTP 201: { ok: true, sessionId: child Session ID }
}
```
Equality only passes the format check; it does not prove an arbitrary event exists or identifies the intended reply. A query is not a lock or CAS token. The server rechecks before fork and still checks whether DSH permits the branch. Do not cache versions indefinitely, especially across Host upgrades or reconnects.
#### Errors and recovery
The query accepts no old coordinates, so a detected migration marker still returns 200. Old references are rejected when reading/writing a timeline, branching, or using import context.
| Request/status | `code` | Client action |
| --- | --- | --- |
| Query, 400 | `PLAY_SESSION_INVALID` | Correct the Session ID. |
| Query, 404 | `PLAY_SESSION_NOT_FOUND` | Refresh the Session selection; this does not mean unknown format. |
| Query, 405 | `PLAY_METHOD_NOT_ALLOWED` | Use GET. |
| Branch, 400 | `PLAY_COORDINATES_INVALID` | Supply a nonnegative safe integer version, not `null`. |
| Timeline/branch, 409 | `PLAY_COORDINATES_MIGRATION_REQUIRED` | Stop reusing old references and follow migration guidance. Do not merely change the version marker and retry. |
Example v2 error: `code` is **top-level** and `error` is a string:
```json
{
"ok": false,
"error": "Session event references need migration. Back up the playthrough and run scripts/migrate-session-coordinates.mjs with the retained source and V4 logs.",
"code": "PLAY_COORDINATES_MIGRATION_REQUIRED"
}
```
Preserve references and backups. Follow the [offline migration guide](DSH_0.1.7_MIGRATION_en.md) to validate source/target logs, preview and apply the mapping, then reread the timeline and messages. The current tool verifies V0/V1/V2/V3→V4 with child-log evidence and Trace references. Unknown future formats need new adaptation and tests; this query does not supply conversion rules automatically.
### Message origin and display semantics
Each message from `GET /sessions/:id/messages` keeps two independent classifications:
- `role` is the model-facing message role, including `user`, `assistant`, `system`, `developer`, and `tool`. DSH runtime context injection may still be `role: "user"` on the model side.
- `origin.kind` is frontend origin/display semantics: `user`, `context`, `steering`, `assistant`, `system`. Third-party frontends must use it to distinguish real user input from context injection and must not draw a user bubble from `role` alone. Whether to hide or present context separately is a frontend choice.
- `tool` and `developer` messages retain their official roles and remain readable as context. Bundled RP does not display them as user bubbles or create separate QA turns. The client accepts these existing roles without additional HTTP response fields.
- `origin.kind: "context"` may include `producer`, `form`, `summary`. Those are bounded optional display metadata. The body stays in `text` / `content` and is not copied into `origin`.
- New fields are additive for older clients. Existing meanings of `role`, `seq`, `text`, `content`, and `incompleteTurn` do not change. When an older server has no `origin`, the client can only fall back conservatively on `role` and cannot reliably detect context injection.
Bundled Mowan does not render reasoning or `origin.kind: "context"` and provides no expand control. Use native DSH **Chat** for runtime detail. Reply swipe is a client composition of branch / user-message / timeline CAS. There is no `/swipe` verb. When the user acts on parent output triggered by `context`, the controller walks forward on the active branch to the nearest real `user` / `steering` and reruns the whole turn from in front of it. It never resends a context report as human input. Missing a real user message fails explicitly. Durable QA provides copy, left/right swipe, new-playthrough branch, same-playthrough rollback, and display-layer edit. Hide is gone. Display regex decides per-segment assistant-body visibility, but actions belong to the QA: multi-segment output still has one action group; if every body is cleared, that group plus non-visual provenance and timeline pointers remain.
Import-context mutation is locked by session-authoritative state. If a DSH user/assistant message, an open turn, or a claimed binding exists, `PUT` / `DELETE` return `409 PLAY_IMPORT_CONTEXT_LOCKED`. Hiding the button is not a substitute. `PUT` rereads the workspace file, runs JSON/schema/hash checks, then sets the binding to `pending`. `GET` may read the summary in any state. Bodies are still read through `/workspace/files?path=` with an explicit path. The first real assembly must carry non-negative `claimEventSeqs` from the public `agent/inbox/spliced` projection before the loader persists the binding as `claimed` and replays the same escaped, `untrusted`, read-only context for that claim identity. A view/assembly without a claim does not inject or consume pending. Only a claimed `turn/end` becomes `consumed`, storing body-free terminal metadata (safe integer end event seq, turn, `reason.kind`). It is not written as DSH history. A DSH provider request retry before the same turn/end reuses the claim. `agent/request-error` does not consume or reset it. Tavern swipe uses the public branch endpoint and, when the fork point is earlier than the source terminal, copies selection and body-free import lineage. The child session needs a new public claim; the old claim is not reused. After interrupt, a new user claim on the original session no longer injects.
Current `GET` and `PUT` of `timeline.json` / `catalog.json` run the same schema/path checks. PUT compares `expectedRevision` then validates inside the same target guard. GET validates after read inside the guard and returns a SHA-256 `revision` of the exact UTF-8 bytes. Missing field → 400 `PLAY_FILE_REVISION_REQUIRED`. Bad format → 400 `PLAY_FILE_REVISION_INVALID`. Target exists/missing/hash mismatch → 409 `PLAY_FILE_REVISION_CONFLICT`. Conflicts do not change the file. Schema failure still returns explicit `PLAY_TIMELINE_INVALID` or `PLAY_CATALOG_INVALID` and never rewrites DSH events. Catalog ids use client-same-origin safe-segment rules, id/path uniqueness, safe POSIX relative paths that must end with `/timeline.json`. Known `ext.pmpDshTavern` fields are validated; unknown third-party fields are kept.
Timeline allows real `qa` nodes only. Greeting is derived from the character card and session selection and does not enter the timeline. Product fields on a QA node are `id`, `kind`, `displayOverride`, optional `parentVariantId`, `adoptedVariantId`, `variants`, and optional `ext`. Undeclared top-level fields are rejected as `PLAY_TIMELINE_INVALID`. Third-party metadata belongs in `ext`. v2 has no hide/suppress-QA field. Tree fields are additive: a node may have `parentVariantId: string | null` pointing at an earlier QA variant; a timeline may have `head: { sessionId, nodeId, variantId }`. `nodeId/variantId` must point at the same QA, but `head.sessionId` may be a continuation session just created by DSH branch that has not produced the next QA yet. The active display path walks ancestors from `head.variantId` via `parentVariantId`. Flat documents without tree fields still read in original node order; the next completed-QA reconcile writes an explicit head/parent. In a tree document, variant ids must be globally unique; a parent may only point at an earlier node and must not cycle. Focus is not a second stored state. Stable focus is derived from timeline head; an empty timeline falls back to catalog `rootSessionId`.
The bundled live client sends only a URL-encoded playthrough id and rejects a mismatched returned id or missing/illegal fields. Stable focus keeps `activeTimelinePath` only as deprecated/ignored old-binding data. Ordinary timeline PUT no longer updates it. Path mutation `lstat`s each segment inside the in-process target lock, rejects symlink/junction, creates layer by layer, and rechecks with realpath. Temp files use exclusive `wx`. Parent directory is rechecked before write and rename. Server CAS is implemented. The bundled live client implements managed revision cache, `null` create-only, 409 cache invalidation, and limited conflict replay (default 3 retries, configurable 1–5). Built-in playthrough lifecycle callers (rename, create catalog append, node metadata/adopt, swipe/rollback timeline head, turn reconcile) use `updateCatalog` / `updateTimeline`. Each mutator recomputes local intent from a fresh document. External session/branch/user-message/directory/timeline create-only side effects are not repeated during CAS replay. An old custom client that only has get/put gets one compatibility fallback and no concurrent-replay guarantee.
### Playthrough lifecycle composition
v2 does not turn character card, playthrough, and greeting into one irreplaceable backend mega-API. A third-party frontend can compose the same flow from public session/workspace/timeline/catalog blocks. Current bundled frontend order:
1. Read the character directory and check whether that character's last playthrough is still empty. Reuse it if empty; otherwise `POST /sessions` creates a real blank DSH session.
2. For an existing source session, copy Tavern selection with v2 `selectionFromSessionId`. Without a source session, bind the card through v1.
3. Create the character/playthrough directory inside the bound workspace root, write an empty `timeline.json`, write catalog, then reread and validate. The generated display name is `Playthrough N` in English and `{number}周目` in Chinese; an explicit rename is stored and displayed verbatim.
Character unbind/rebind is the exception: `POST /v1/character-selection` checks whether the session belongs to a playthrough whose character does not match, before writing selection. On conflict it returns 409 `CHARACTER_PLAYTHROUGH_DETACH_REQUIRED`. `error.details.conflicts[]` includes `playthroughId`, `playthroughTitle`, `sessionId`, `expectedCharacterId`, `requestedCharacterId`, and `descendantSessionCount`. Selection is not written. After user confirm, the frontend should call `POST /v2/playthroughs/:id/detach-session` per conflict, then retry the original v1 request. Detach is computed by the server from the timeline tree. Clients must not guess or rewrite descendant relations.
Detach deletes every variant of the target session and every descendant variant that has them as parent. Surviving nodes are not rehung. Sibling swipes and other branches on the same node remain. If root is removed, catalog clears `rootSessionId` and the old import-context reference but keeps the playthrough row, name, and number. The next new playthrough for that character creates a new DSH root session for the empty playthrough and reattaches it with catalog CAS. It does not create a new directory or number. No DSH session is deleted, archived, or renamed.
This “playthrough transaction” is a frontend composition of public atomic operations, not a server cross-file transaction. A single client's controller serializes same-character creates. Built-in callers use limited server-CAS replay against cross-tab writes, but the session/directory/timeline/catalog combination is still not a transaction. A mid-create failure is not wrapped in a cross-file transaction: workspace bind, directory create, ordinary-file and catalog/timeline writes, playthrough detach, and session create/branch/user-message plus import-context PUT/DELETE each write `ctx.logger` with one `operationId` inside that mutation request. Clients recover from completed stages, read-back, and stable error codes. Different API requests do not share an operationId. chrome, GET, and browser frontend operations stay quiet. Do not advertise the composed flow as an atomic commit.
### Imported-record context
An empty playthrough's opening dock binds an imported record to the current root session. It does not create another session or write greeting or timeline nodes. Bind, rebind, and unbind use `PUT` / `DELETE /sessions/:id/import-context`. The client shows the actions in the same footer and, after bind, previews the last three QA turns. While `pending`, the first real request injects the full content. The loader expands Tavern macros in greeting and QA with user/character names from the same profile snapshot, then escapes them and marks them `untrusted` read-only context so ST placeholders do not enter DSH prompt-variable parsing. It does not forge user/assistant QA or copy imported content into the Tavern timeline; the system prompt used by a real request is still persisted in official DSH history. The first assembly must establish a durable claim from the original user turn/event's public `claimEventSeqs`. The same claim identity may reassemble. Unclaimed pending is not consumed by a view or an unrelated turn/end. retry/swipe lineage and cancel/interrupt terminals are implemented: terminal stores only event seq, turn, `reason.kind`, and similar body-free metadata. The same request may reassemble before terminal; after terminal a new claim no longer injects. Tavern swipe copies selection/lineage only through the public branch seam and does not claim to intercept every third-party native fork.
The body is `{ reference: { path, expectedHash? } }`. The file must sit inside the bound workspace root, with `schemaVersion: 1` and a `qa` array. The import parser does not apply a 256 KiB or QA-count artificial context cap, and it does not summarize or slice. Whether the model context overflows is left to DSH/provider. Generic `/workspace/files` still has a 1 MiB file-layer read/write limit. Ordinary SillyTavern JSON/JSONL can be parsed by the client and written as that context file. The public import/export surface defines no portable bundle. ST JSONL expresses only the current active linear history and cannot store the full playthrough tree. Greeting remains a display projection and does not forge assistant history.
### v2 persistence, concurrency, and audit guarantees
- History paginates until `hasMore: false`. An empty Host page, illegal oldest `seq`, or a repeating/non-advancing cursor returns 502 `PLAY_HISTORY_CURSOR_STALLED`. The plugin does not summarize or slice.
- Public `claimEventSeqs` drive import-context pending → claimed. The same identity may replay before terminal. `turn/end` stores body-free metadata (event seq, turn, `reason.kind`) and becomes consumed. After terminal a new claim does not inject. Tavern swipe copies selection and body-free pending lineage through public branch; the old claim is not reused. Third-party native forks are outside plugin interception.
- catalog/timeline GET returns exact UTF-8-byte SHA-256 `revision`; PUT uses explicit `expectedRevision`. Missing/bad format are 400; target-state or hash mismatch is 409; conflicts do not change the file. The bundled live client caches revisions and limits CAS replay to pure document intent.
- catalog/timeline run the same schema/path checks after GET read and before PUT write. Unknown third-party `ext` is kept. revision/CAS share the same target guard. Path locking performs per-segment no-follow checks, uses exclusive `wx` temporary files, and rechecks the parent before rename.
- Paths reject symlink/junction types exposed by Node, create directories non-recursively, and recheck with realpath. Pure Node cannot resist an extremely narrow race from an external process; the contract adds no native addon.
- Backend `ctx.logger` operation logging covers `PUT /workspace`, `POST /workspace/dirs`, `PUT /workspace/files?path=`, playthrough detach/relink, session create/branch/user-message, and import-context PUT/DELETE. Each mutation request uses one `operationId` for stages and terminal status. Resource/chat bodies are never logged. user-message records only the Host prompt-accepted stage, not body, length, or summary. Read-only GET/list, session/messages/focus/import-context, and chrome stay quiet; persistent journal queries and exports are documented below; browser log uploads remain outside the contract.
`npm run verify:2.0` checks history, schema/CAS/focus/path jail, claim/lineage,
content-free operation logging, chrome service/slot, workspace admission, localization, and
package boundaries. With `DSH_TAVERN_PLAY_LIVE=1` and `DSH_TAVERN_PLAY_LIVE_URL`, it also
reads chrome/workspace authority from a running DSH Host. This read-only smoke does not replace
real-write or browser two-tab notification checks.
`chrome` is the whole frontend's blue/red orb. It lives in plugin data `chrome.json` and defaults to `native`. Illegal `mode` → 400. GET does not require JSON Content-Type.
`GET /chrome/events` is Tavern-owned SSE, not a DSH Host API. On connect it immediately sends `event: chrome/change` with the current snapshot. A successful `PUT /chrome` broadcasts the same event once after mode actually changes. Event data is only `{ mode, revision }`. SSE uses `text/event-stream`, disables cache, and clears subscribers on close. Non-GET → 405. Older clients that only read `mode` stay compatible. Clients that cannot use SSE should fall back to GET/focus refresh or short polling. Direct edits of `chrome.json`, other-process writes, and DSH private transport are outside this event contract.
The client entry always shows `DT`. Left-click immediately expands or collapses the menu. Rapid repeated clicks repeat that default. Double-click has no special effect. Right-click switches frontend display mode. Menu buttons say **Switch to custom frontend mode** / **Switch to DSH native mode**. Current state may show **Current: Mowan** / **Current: DSH native**. The tooltip is always **Switch frontend display mode**. The menu stays mounted; content fades in after the 220ms expand.
The `PUT /workspace` directory must already exist (`workspaceController.create` also does not mkdir). `POST /workspace/dirs` is created by `PlayWorkspaceStore` inside the bound root for character/playthrough subdirectories. It does not invoke the global directory picker, so native/browse Hosts both work. The path jail rejects `..`, absolute paths, symlinks that point outside the root, and file conflicts. files/dirs return 409 when no root is selected. Do not use archive to tuck sessions away. The `user-message` body is not a full prompt. The loader's Play Host adapter explicitly calls `sessionController.create()`, `rename()`, `fork()`, `prompt()`, `inspect()`, and `page()`. One history read pins its first inclusive `throughSeq` across every page. `PUT /workspace` calls `workspaceController.create()`. Fork of an open turn maps to HTTP 409.
## v1 bundled UI contract
Prefix: `/pmp-dsh-tavern/api/v1`. `/dsh-tavern/api` is not part of the current contract.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/presets?sessionId=` | Preset catalog and current selectedId | Implemented |
| POST | `/presets` | Create a preset; returns preset | Implemented |
| GET | `/presets/:id` | Complete current preset; returns preset | Implemented |
| PUT | `/presets/:id` | Update preset; returns preset | Implemented |
| DELETE | `/presets/:id` | Delete preset and clean up bindings | Implemented |
| POST | `/import` | Import an ST preset; content is a JSON string, name is optional | Implemented |
| POST | `/select` | { id, sessionId? }; select/clear preset, omitted sessionId targets global selection | Implemented |
| GET | `/active?sessionId=` | Current assembly preview summary: sessionSelection, worldBookSelection, resources, callConfig, audit; runs assembly, not a historical record | Implemented |
| GET | `/presets/:id/export` | Export current edited state as an ST JSON attachment | Implemented |
| GET | `/presets/:id/regex-scripts` | Read preset native regex array | Implemented |
| PUT | `/presets/:id/regex-scripts` | Replace the complete preset native regex array | Implemented |
| GET | `/presets/:id/world-books` | Read preset ordered linked standalone world-book IDs | Implemented |
| PUT | `/presets/:id/world-books` | Replace the complete preset ordered linked standalone world-book IDs | Implemented |
| GET | `/characters/:id/regex-scripts` | Read character native regex array | Implemented |
| PUT | `/characters/:id/regex-scripts` | Replace the complete character native regex array | Implemented |
| GET | `/characters/:id/world-books` | Read character ordered linked standalone world-book IDs | Implemented |
| PUT | `/characters/:id/world-books` | Replace the complete character ordered linked standalone world-book IDs | Implemented |
| GET | `/characters` | Character catalog, sorting and missing-card summaries | Implemented |
| POST | `/characters` | Create a character card | Implemented |
| POST | `/characters/import` | Import a JSON/PNG character card; 32 MiB file limit, 16 MiB decoded limit per PNG card metadata chunk (also applies to export) | Implemented |
| GET | `/characters/:id` | Complete current character card; returns character | Implemented |
| PATCH | `/characters/:id` | Update character fields | Implemented |
| DELETE | `/characters/:id` | Delete card and clear bindings, retaining a missing-card summary | Implemented |
| GET | `/characters/:id/json` | Export a character JSON attachment | Implemented |
| GET | `/characters/:id/png` | Export a character PNG attachment | Implemented |
| PATCH | `/characters/:id/world-book` | { characterBook }; update embedded lore, not standalone book bindings | Implemented |
| PUT | `/characters/order` | { mode, characterIds? }; configure ordering | Implemented |
| POST | `/characters/relink` | { previousCharacterId, characterId }; recover missing-card references | Implemented |
| GET | `/character-selection?sessionId=` | Current character binding/options (including greeting index) and card summary | Implemented |
| POST | `/character-selection` | { sessionId, characterCardId, character? }; bind or unbind | Implemented |
| GET | `/world-books` | Standalone world book catalog | Implemented |
| POST | `/world-books` | Create standalone world book | Implemented |
| GET | `/world-books/:id` | Complete current standalone world book; returns worldBook | Implemented |
| PATCH | `/world-books/:id` | Update standalone world book | Implemented |
| DELETE | `/world-books/:id` | Delete standalone world book and clean up related bindings | Implemented |
| GET | `/users` | User catalog | Implemented |
| POST | `/users` | Create user | Implemented |
| POST | `/users/import` | Import Tavern user JSON with a new ID and no session binding | Implemented |
| GET | `/users/:id/export` | Export the saved name, description and optional avatar as a Tavern JSON attachment | Implemented |
| GET | `/users/:id` | Complete current user; returns user | Implemented |
| PATCH | `/users/:id` | Update user | Implemented |
| DELETE | `/users/:id` | Delete user and clean up related bindings | Implemented |
| POST | `/world-books/import` | Import world-book JSON | Implemented |
| GET | `/world-books/:id/json` | Export a world-book JSON attachment | Implemented |
| GET | `/world-book-selection?sessionId=` | Explicit Session world-book bindings and resource summaries | Implemented |
| POST | `/world-book-selection` | Set explicit Session worldBookIds | Implemented |
| GET | `/user-selection?sessionId=` | Current user binding and user document | Implemented |
| POST | `/user-selection` | Set or clear the Session user binding | Implemented |
| GET | `/users/:id/world-books` | Read standalone world-book IDs linked to a user | Implemented |
| PUT | `/users/:id/world-books` | Replace the complete user worldBookIds | Implemented |
| GET | `/ui-settings` | Read language, scale and character-follow RP | Implemented |
| PUT | `/ui-settings` | Save language, scale and character-follow RP | Implemented |
| DELETE | `/ui-settings` | Reset language, scale and character-follow RP | Implemented |
| GET | `/conversation-settings` | Read Mowan text/action scale | Implemented |
| PUT | `/conversation-settings` | Save Mowan text/action scale | Implemented |
| DELETE | `/conversation-settings` | Reset Mowan text/action scale | Implemented |
| GET | `/rp-policy` | Read RP policy text | Implemented |
| PUT | `/rp-policy` | Save RP policy text | Implemented |
| DELETE | `/rp-policy` | Reset RP policy text | Implemented |
| GET | `/rp-mode?sessionId=` | Read RP, pending and followCharacter | Implemented |
| PUT | `/rp-mode` | { sessionId, active }; request an RP change | Implemented |
| GET | `/rp-alert?sessionId=` | Read an unconsumed RP risk alert | Implemented |
| DELETE | `/rp-alert?sessionId=&id=` | Consume the alert; id is optional | Implemented |
| GET | `/traces?sessionId=` | Legacy historical metadata audit; returns records/storage/authority without complete prompt bodies | Implemented; retained for compatibility |
| GET | `/session-templates` | Template catalog, selection, content summaries and missing-resource diagnostics | Implemented |
| POST | `/session-templates` | `{ name, sourceSessionId }` captures session settings, or `{ name, selection? }` creates explicitly; omitted selection creates a blank template | Implemented |
| POST | `/session-templates/import` | Import Tavern template JSON with a new ID, preserving the selected template | Implemented |
| GET | `/session-templates/:id/export` | Export saved template settings and resource references as a Tavern JSON attachment | Implemented |
| GET | `/session-templates/:id` | Template detail and resource diagnostics | Implemented |
| PATCH | `/session-templates/:id` | Update name, explicit selection, or capture session settings using sourceSessionId | Implemented |
| DELETE | `/session-templates/:id` | Delete template | Implemented |
| POST | `/session-templates/select` | { id }; select or clear a template | Implemented |
| POST | `/session-configurations/preview` | { source }; read-only current/template configuration preview; does not run prompt assembly | Implemented |
| POST | `/session-configurations/apply` | { targetSessionId, source }; validate and apply bindings | Implemented |
Resource/subresource details follow below. Except where explicitly specified, do not assume unsupported methods return v2-style 405 errors.
### Portable user and template JSON
These resources stay within v1's current-resource boundary; no configuration aggregate or resource-bundle
API is added. Presets continue using SillyTavern preset JSON, character cards use JSON/PNG, and standalone
world books use world-book JSON. Users and session templates use versioned Tavern JSON. Send the document
directly as the `application/json` request body, without a `{ content: "..." }` wrapper:
```json
{
"format": "pmp-dsh-tavern",
"version": 1,
"resourceType": "user",
"data": { "name": "Reader", "description": "A curious traveler." }
}
```
The user envelope is incompatible with SillyTavern's whole-list persona backup (`personas`,
`persona_descriptions`, and `default_persona`). `POST /users/import` does not accept that ST backup
format, and the output of `GET /users/:id/export` cannot be passed directly to ST Restore. See
[User resource formats](USAGE_en.md#5-users).
Templates use the same envelope with `resourceType: "session-template"` and
`data: { "name": "Example", "selection": { "presetId": null, "characterCardId": null,
"userId": null, "worldBookIds": [], "character": {}, "rp": { "active": false,
"source": null, "followSuppressed": false, "sandboxBefore": null } } }`.
- Each import allocates a new ID. Imported data cannot specify `id`, replace a same-name resource, or
automatically bind a session. Template import also preserves the selected default template; ordinary
template creation still selects the newly created template.
- User exports contain only the saved name, description and optional avatar, excluding user–world-book relations and
session bindings. Template exports contain name and selection, including ordered resource IDs,
character options, and saved RP fields. They exclude resource bodies, history, Trace, Inbox, and old
runtime state. Transfer other content through existing resource/relation APIs; IDs are not remapped by name.
- Export returns a JSON attachment directly, without an `ok:true` wrapper. User import returns HTTP 201
`{ ok:true, user }`; template import returns HTTP 201 `{ ok:true, template, contents, diagnostics }`.
Saving/importing a template with missing resources is allowed; diagnostics identify the missing
references, and configuration apply still rejects them.
- Unsupported versions, types, fields, or invalid values return 400; oversized bodies return 413.
User import is limited to 1 MiB, names to 200 characters, and descriptions to 100,000 characters;
other user JSON writes retain their 256 KiB body limit. Template import is limited to 256 KiB;
other template JSON writes retain their 64 KiB limit. Larger import limits allow valid escaped JSON
text to roundtrip. Template count limits return 409; storage-byte limits return 413.
### Explicit template editing and sampling overrides
`POST /session-templates` does not require a current session: `{ "name": "Blank" }` creates a blank
template; supplying `selection` saves explicit settings. `PATCH /session-templates/:id` can update the
name, replace selection, or capture configuration from `sourceSessionId`. `selection` and
`sourceSessionId` are mutually exclusive. Explicit selection replaces the entire template configuration;
omitted selection fields use blank defaults rather than merging with the previous configuration.
Creating/editing a template does not change existing session bindings. Name-only updates preserve selection.
Selection fields are `presetId`, `characterCardId`, `userId` (null or a nonempty ID of at most 200 characters),
`worldBookIds` (ordered, unique, at most 100 IDs), `character`, and `rp`. `character` may contain a nonnegative
safe-integer `greetingIndex`, and boolean `preferCharacterSystemPrompt` / `preferCharacterPostHistory`.
`rp` may contain boolean `active` / `followSuppressed`, `source` of null / `command` / `character-follow`,
and `sandboxBefore` of null / `read-only` / `workspace-write` / `danger-full-access`. Template names are
limited to 120 characters and 480 UTF-8 bytes. Explicit edits and imports reject invalid fields instead
of silently dropping them; existing diagnostics check resource availability separately.
Preset `sampling.reasoningEffort` supports `off`, `low`, `medium`, `high`, `xhigh`, and `max`.
Import/save/export also preserve other provider effort IDs that contain at most 100 characters,
are not whitespace-only, and contain no control characters. The editor displays and preserves such
values even when they are absent from its common choices. Omitting the field during update preserves
its saved value; explicit null (the UI's inherit option) clears the Tavern override. Successful resource
storage does not establish provider support; the actual request follows the target model/provider's
capabilities and defaults.
### Historical world-book audit
`GET /traces?sessionId=` retains `records[].worldBooks[]` as a public compatibility surface.
Each book has `resource`, `budget`, and `decisions[]`. Decisions include entry identity/name,
included/rejected status, reason, primary/secondary keys and matches, group, probability,
tokenCost, and requested/applied position fields. `decisions[].entryId` is the stringified
in-book UID; resourceId identifies the book, and entryName is its entry comment/name, which
may be empty. The summary retains at most 16 books and 128 decisions in total. Entry IDs over
120 UTF-16 units and resource IDs over 200 units are clipped with an ellipsis; this is not a
complete resource inventory. See [world-book entry identity](PROMPT_API_V3_en.md#world-book-entry-identity)
for new v3 source IDs and earlier candidate compatibility.
### Current configuration and resource reads
To inspect bindings without running assembly, preview the current configuration,
then fetch the required resource detail. The caller supplies an explicit sessionId.
v1 errors may be strings or structured objects.
```js
const base = '/pmp-dsh-tavern/api/v1';
async function readJson(path, options = {}) {
const response = await fetch(base + path, { credentials: 'same-origin', ...options });
const data = await response.json();
if (!response.ok || !data.ok) {
const message = typeof data.error === 'string' ? data.error : data.error?.message;
throw new Error(message ?? `HTTP ${response.status}`);
}
return data;
}
const preview = await readJson('/session-configurations/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: { mode: 'current', sessionId } }),
});
const characterId = preview.selection.characterCardId;
const character = characterId === null ? null
: (await readJson(`/characters/${encodeURIComponent(characterId)}`)).character;
```
`preview` returns `{ok,selection,contents,diagnostics,available}`. `contents` contains
resource references and summaries, not complete documents. `available:false` reports
invalid/missing resources; a missing resource detail can still fail independently.
This preview neither creates a Session nor applies bindings. Read preset/character/
user linked world-books through their corresponding subresources; embedded lore is
part of the character document. These reads do not promise a cross-request snapshot.
### Character-card sidebar order
The character list has three explicit sort modes: `updated` by `updatedAt` descending (then name, ID), `name` A→Z (Chinese uses `zh-CN` collation), `custom` by user drag order. Drag writes library state only. It does not change the card source or `updatedAt`. Switching modes does not clear a saved custom order.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| PUT | `/characters/order` | Request: `{ mode, characterIds? }`; response: `{ ok: true, characters: [...], sorting: { mode } }` | Implemented |
`mode` must be `updated`, `name`, or `custom`. `custom` without `characterIds` switches back and restores the saved custom order. If none is saved, the current resource order is initialized and cards added while away are appended. Send `characterIds` only for a real reorder. It must contain every stored character-card ID exactly once, at most 4096 items. Unknown, duplicate, or missing IDs return 400 and leave state unchanged. Other modes reject `characterIds`. Successful mode and custom order are stored separately in `character-state.json` as `characterSortMode` and `characterOrder`. In custom mode, newly created or imported cards append. Deleting a card also removes its order entry. `GET /characters` also returns `sorting: { mode }`.
### Missing character cards and relink
Deleting a card still deletes the resource body and cover and still clears stale session selections. The library also keeps a bounded tombstone (original ID, display name, source-file SHA-256 when available) so playthroughs that still reference the card can appear under the human-readable **Missing character cards** area. New playthroughs also snapshot `characterId`, `characterName`, and `characterSha256` when available in `ext.pmpDshTavern`. Card bodies are not copied into the timeline.
After re-import, unique SHA-256 is matched against the tombstone first. Without a hash match, normalized same-name matching is used only when both the missing name and the live name are unique. A unique match relinks automatically. Several same-name candidates are not guessed; the sidebar chooses the target.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| POST | `/characters/relink` | Request: `{ previousCharacterId, characterId }`; response: `{ ok: true, relinkedPlaythroughCount, relinkedSessionCount }` | Implemented |
v1 `/characters/relink` is the missing-resource recovery surface. It uses catalog revision as CAS, moves every playthrough that referenced the old ID onto the target card, and updates root/swipe/branch descendant sessions in one batch of session-selection writes. v2 `/playthroughs/:id/relink-character` is the playthrough-lifecycle surface: it migrates only that playthrough and all of its descendant sessions. The bundled frontend evaluates the target with “unique SHA-256, then both-sides unique same name”. A mismatch shows a warning, but the user can still confirm. The backend does not veto an explicit choice with heuristics.
Both relink paths refuse to overwrite a third card binding that is unrelated to the playthroughs being migrated. If the session batch write fails, catalog is rolled back with the just-written revision. v1 recovery clears the tombstone only after success. When no workspace is bound, automatic recovery is deferred and the tombstone is not silently dropped. Key start, success, and deferral reasons go to `ctx.logger`.
### Conversation display settings
`/conversation-settings` is a v1 bundled-UI contract, separate from `/ui-settings`. It persists only Mowan RP conversation display preferences. It does not enter profile, prompts, timeline, DSH history, or export bodies.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/conversation-settings` | Request: none; response: `{ ok: true, settings: { schemaVersion: 1, textScale, actionScale, bubbleStyle?, interactiveCards?, scriptEnablement?, renderingAdapters? } }` | Implemented |
| PUT | `/conversation-settings` | Request: `{ textScale, actionScale, bubbleStyle?, interactiveCards?, scriptEnablement?, renderingAdapters? }`; response: same as GET | Implemented |
| DELETE | `/conversation-settings` | Request: none; response: restore both fields to `1` | Implemented |
`scriptEnablement` stores ordinary local choices as `{ schemaVersion: 1, entries: [{ owner, key, enabled }] }` (up to 128 entries). Owners are character/preset resource IDs or the SHA-256 identity of the canonical RP workspace for global dependencies. Helpers prefer a unique original ID; otherwise a source SHA-256 identifies unique content. Duplicate sources use a source-plus-name digest only when their names are unique; order never participates in identity. Changed unidentified source or disambiguating names inherit source defaults. Indistinguishable duplicates cannot save per-entry overrides and retain their individual source defaults until unique IDs are added to the source card. New resource IDs and source-card exports do not carry local choices. Omission or DELETE clears overrides; the appearance reset action preserves script choices and the master switch. These preferences persist script switches only. Source or choice changes require updating selected dependencies. Downloaded, enabled scripts can read and write current bound variables; internal execution bindings are not persisted.
`renderingAdapters` stores ordinary compatibility-mode overrides as `{ schemaVersion: 1, entries: [{ owner, source, mode }] }` (up to 128 entries). `source` is an HTTPS source; `mode` is `builtin` or `original`. Omitting an entry selects a local capability contract by exact import alias and supported invocation semantics. `builtin` explicitly requests local capabilities and stops if unsupported; `original` selects the original code graph. These preferences contain no approval, digest, source code or write grant. Built-in MVU neither downloads nor executes upstream modules; original code still requires downloading and cached-content digest verification. Owners are isolated, copies do not inherit choices, appearance reset preserves this preference, and omission or DELETE clears overrides.
Optional `bubbleStyle` and boolean `interactiveCards` follow the [presentation protocol](CONVERSATION_PRESENTATION_en.md); body limit is 384 KiB. Omission or DELETE restores their defaults. User create/PATCH/import/export supports optional raster-data-URI `avatar` up to 128 KiB; null clears it, omission on PATCH preserves it. Timeline `ext.pmpDshTavern.appearance` follows the same document, rejects invalid metadata with `PLAY_APPEARANCE_INVALID`, and uses existing revision/CAS writes.
Both scales are finite numbers from `0.75`–`1.5` in steps of `0.05`. PUT is a full replace and rejects unknown fields. `textScale` applies to Mowan user/assistant bodies and greeting (including the empty-playthrough opening dock). `actionScale` applies only to the copy, swipe, branch, rollback, and edit row at the end of a durable QA.
### Preset export
`GET /presets/:id/export` returns an `application/json` attachment. The server starts from the saved ST original, keeps unknown top-level fields, prompt fields, other `prompt_order`s, and extensions, then overlays name, sampling, prompt content/order/enablement, and the current Chat Completion order from Tavern's normalized state. Export therefore reflects current edits and is not a raw download of import-time `source.raw`.
The body is Chat Completion preset JSON that can be sent back to `POST /import` or imported into SillyTavern. Tavern-only `systemPromptMode` has no ST field and is not written. Native regex carried by the resource stays on its original ST path. This GET does not change selection, session, resources, or the operation log.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/presets/:id/export` | Request: none; response: ST JSON attachment; `Content-Disposition: attachment` | Implemented |
### Native ST regex carried by a resource
Presets and character cards share the same sub-resource contract:
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/presets/:id/regex-scripts` | Request: none; response: `{ ok: true, regexScripts: [...] }` | Implemented |
| PUT | `/presets/:id/regex-scripts` | Request: `{ regexScripts: [...] }`; response: `{ ok: true, regexScripts: [...] }` | Implemented |
| GET | `/characters/:id/regex-scripts` | Request: none; response: `{ ok: true, regexScripts: [...] }` | Implemented |
| PUT | `/characters/:id/regex-scripts` | Request: `{ regexScripts: [...] }`; response: `{ ok: true, regexScripts: [...] }` | Implemented |
`PUT` is a full ordered-array replace, not a field merge. Array order is execution order inside that resource. Elements must be objects. The server does not rewrite native ST fields and does not drop unknown per-rule extensions. The adapter prefers the resource's existing `regex_scripts` path. If there is no array yet, presets write `extensions.regex_scripts`, V2/V3 cards write `data.extensions.regex_scripts`, and V1 cards write `extensions.regex_scripts`. Other resource fields stay unchanged. Writes still go through the matching store's atomic save and total-document size limit.
This v1 sub-resource edits only the preset or card original. It does not compose global regex, compute the session's final effective set, or change history, timeline, or the AI request. The Mowan display pipeline reads the saved resource data only as a render projection. Failures reuse the parent resource API's existing format and status codes.
Mowan regex-page create/import/edit/delete for **preset-bound / character-bound** uses these native sub-resources. Rules are not saved into the workspace global `ui/regex.json` with only a resource scope. Local scoped rules already saved that way enter a pending-migration state the next time the matching resource is the current binding. After the user clicks save, rules are written to the resource original and removed from the global document. Migration de-duplicates by rule id against existing native rules and does not rewrite the resource merely because the panel was opened.
### Preset/character standalone world-book relations
A preset or character card may bind zero or more existing standalone world books:
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/presets/:id/world-books` | Request: none; response: `{ ok: true, binding: { presetId, worldBookIds } }` | Implemented |
| PUT | `/presets/:id/world-books` | Request: `{ worldBookIds: [...] }`; response: same as GET | Implemented |
| GET | `/characters/:id/world-books` | Request: none; response: `{ ok: true, binding: { characterCardId, worldBookIds } }` | Implemented |
| PUT | `/characters/:id/world-books` | Request: `{ worldBookIds: [...] }`; response: same as GET | Implemented |
`PUT` fully replaces that resource's ordered relation. Duplicate IDs are de-duplicated stably. Missing resources or world books reject the write. Each preset or card may bind at most 100 books. Relations are atomically stored in loader-owned `resource-world-book-bindings.json`. No Tavern-private field is written into the ST preset or card original. Therefore:
- Preset export and card JSON/PNG export do not carry these Tavern-local relations. After uninstall, the original resources still work with native ST semantics.
- `DELETE /presets/:id` or `DELETE /characters/:id` clears relations owned by that resource. Deleting a standalone world book clears the matching ID from session, user, preset, and character relations.
- The plural character path `/world-books` means standalone-resource relations. The singular `/world-book` still edits the card's own `character_book`. Both may exist at once.
The loader's standalone-book composition order is fixed: session explicit, user relation, preset relation, character relation. The same ID runs once, but audit/resource summary keeps every `bindingSources`. The card's embedded `character_book` then enters the same matcher. `GET /active` `worldBookSelection` publishes `explicitIds`, `userBoundIds`, `presetBoundIds`, `characterBoundIds`, `effectiveIds`, `duplicateIds`, and `order`.
The world-book panel lists these sources directly. When the current card has no `character_book`, the frontend may first create a `{ name, entries: [] }` draft and save it with existing `PATCH /characters/:id/world-book`. That creates an exportable embedded book. It is not the same as binding a standalone book.
### Tavern playthrough-branch composition
The bundled RP view does not overlay native DSH fork. It calls public `POST /sessions/:id/branch` at the adopted assistant's `endEventId`, verifies with `/messages` that the child session inherited that durable user/assistant range, then creates a new playthrough directory and a timeline copy through that QA. The copy only redirects the target adopted variant's `sessionId` to the child session, then catalog CAS appends the new playthrough and focus-by-id validates. The new playthrough keeps DSH-authoritative context. Re-entering from the sidebar opens the continuation branch session. Source timeline, source variant, and original DSH messages are unchanged.
This is still a client composition of existing atomic APIs, not a mega-transaction across session, directory, timeline, and catalog. If branch succeeds and a later file write fails, an unarchived DSH child session or workspace orphan file may remain. Each step is recorded by the existing `ctx.logger` operation. The client does not fake rollback by deleting original history.
**Continue from here in this playthrough** reuses the same DSH branch and inherited durable-range checks, but creates no directory, timeline copy, or catalog row. The client only moves `head` to `{ continuationSessionId, targetNodeId, targetVariantId }` with timeline CAS. Later nodes stay in the tree but are not on the active path. After the next real message completes, a new QA is appended with the target variant as parent. It does not delete or rewrite DSH history and does not forge user or assistant messages.
### Backend operation-log utility
Production code uses the Tavern-owned event contract; low-level `createOperationContext` remains available for existing callers. Stage names no longer define public event semantics. See the [operation log contract](OPERATION_LOGS_en.md).
### Persistent operation logs and frontend contract
`GET /pmp-dsh-tavern/api/v2/operation-logs` provides filters, pagination and single-page JSONL export. Query fields, record format, stable events, capacity and upgrade rules are maintained in the [operation log contract](OPERATION_LOGS_en.md).
### Sessionless opening drafts
Under the v2 Play base, `POST /drafts` saves `{ characterId, source?, selection?, assemblyPresetId? }` without a DSH Session. `GET /drafts/:id` returns `{ draft, playthrough }`. `PUT /drafts/:id` requires `expectedRevision` and accepts selection, assemblyPresetId, variables, importContextRef, or resetVariables. It never changes the character identity. `POST /drafts/:id/preview` accepts `{ expectedRevision, preset? }` and reads this opening draft's current resources, variables and caller-supplied assembly rules; omitted preset uses the draft snapshot. It returns `{ ok, preview }` with `scope:'opening-draft'`, `draftId`, `draftRevision` and `pendingInputsIncluded:false`. It creates no native Session, writes no history and calls no model. Configuration changes or first-send preparation return 409.
`POST /drafts/:id/materialize` takes `{ expectedRevision, operationId, text }`, prepares one durably reserved Session and returns `{ sessionId, requestId, accepted }`; it does not send a model request. Call the existing `/sessions/:id/user-message` with the same text and requestId unless already accepted. `POST /drafts/:id/cancel` either releases a cleared unbound blank and restores the draft with `draft.lastInput` retained for reopening, or stops an already admitted real Session while preserving history. Draft scope for MVU is `{ mode:"draft", playthroughId, characterId, greetingIndex, selectionToken? }`, with no sessionId. Card write grants require the exact current selection token and ready source execution.
## v3 prompt assembly audit
Prefix: `/pmp-dsh-tavern/api/v3`. Read-only prompt assembly records and provenance.
| Method | Path | Behavior | Status |
| --- | --- | --- | --- |
| GET | `/capabilities` | Contract capabilities, source mapping and capacity limits | Implemented |
| GET | `/sessions/:id/assemblies` | Historical index without section/context/system-message bodies | Implemented |
| GET | `/sessions/:id/assemblies/:recordId` | Cold-read official history; verified section/context bodies; schema 4 returns source metadata/hash/counts while legacy schema 3 detail may retain stored `source.text` | Implemented |
Fields, examples, errors and persistence: [v3 detailed contract](PROMPT_API_V3_en.md).
`/sessions/:id/sources` is not part of the v3 contract and returns 404. Read current configuration and complete resources through v1.
## Browser chrome mode service
The Tavern client registers the stable service name `pmpDshTavernChrome` through DSH `0.2.1-rc.2` / `0.2.1-rc.3` public Cordis `ctx.provide`. This is a Tavern v2 contract, not a DSH Host API. It provides only the `native|play` lifecycle. It does not own or arbitrate any slot, view, or third-party plugin UI.
Public face:
- `getMode()`, `getSnapshot()`: synchronously return a frozen `{ mode, revision }`. On an older server, revision may be `null`.
- `subscribe(listener)`: notify the current snapshot immediately and return an idempotent disposer.
- `refresh()`: read back and commit the authoritative snapshot via `GET /v2/chrome`.
- `setMode(mode)`, `switchMode()`: serialize writes; update local state only after `PUT /v2/chrome` succeeds.
- `when(mode, setup)`: setup on enter; dispose on leave, unregister, or service unload. A late async setup is cleaned up immediately.
A required-dependency plugin may declare `inject: ['pmpDshTavernChrome']` and read `ctx.pmpDshTavernChrome`. An optional consumer should use `ctx.get('pmpDshTavernChrome')` and keep its native/fallback behavior when missing. Third parties must not `provide` the same name again, and must not depend on Tavern-internal React state, `playSlots`, EventSource, or timers.
Internal transport defaults to an initial `GET /v2/chrome`, window-focus read-back, and 1-second polling to synchronize snapshots without an additional persistent SSE connection per page. Polling and focus share an unfinished read so network congestion cannot accumulate repeated requests. `GET /v2/chrome/events` remains available to explicit SSE consumers. When an EventSource implementation is explicitly supplied to the internal adapter, connection failure or disconnection starts polling, which stops after SSE recovers. BroadcastChannel is not in the contract. Service unload stops transport and clears every `when` effect. Several third-party plugins register independently and each cleans only its own slots/UI.
Full notes on third-party DSH plugins, standalone web clients, surface ownership, atomic action composition, and uninstall fallback: [FRONTEND_INTEGRATION_en.md](FRONTEND_INTEGRATION_en.md). There is no one-click config-file Mowan replacement, frontend provider registry, or dynamic bundle loader.
## Rendering execution bindings
These internal routes bind available, enabled card execution to its exact source and scope. They do not introduce a user-facing approval state. The legacy `downloaded:true` field means execution sources are ready, including local MVU initialization code; the execution-bundle digest does not imply an upstream bundle was downloaded or verified.
| Method | Path (under v1) | Input / result | Status |
| --- | --- | --- | --- |
| POST | `/rendering-write-grants` | Trusted renderer submits `{source,sourceIdentity,executionId,downloaded:true,enabled:true}`. Source is the full execution bundle JSON; identity is `{version:1,sha256,scope}`. Host recomputes SHA-256; returns `{ok:true,grantId,sourceIdentity}` | 200; invalid input/identity 400; oversized request 413 |
| DELETE | `/rendering-write-grants/:grantId` | Stop an internal binding; repeated removal is idempotent | 200 |
Existing local peer, Host, Origin/desktop-token, JSON media-type and DSH admission checks apply. `tavernRenderingAuthority.resolve({grantId,sourceIdentity})` returns null or `{valid:true,write:true,scope}`; synchronous `isCurrent` is checked immediately before MVU commit. At most 64 live execution bindings are held in memory; no source body is stored. Bindings have no approval expiry and are cleared on unload. Script switches/runtime cleanup revoke them; local capabilities or persisted downloads recreate fresh current bindings on refresh. They do not bypass scope, source schema, CAS or idempotency. Native card variables emit facts for manager observation; manager model/store/retrieve policies remain separate. See [MVU](MVU_en.md).
Standard versus optional core capabilities, migration and evidence are defined in [assembly strategies](REQUEST_ASSEMBLY_en.md). Check capabilities() and requireAvailable(preset) for the selected backend.
## Rendering cache storage
The v1 prefix `/pmp-dsh-tavern/api/v1/rendering-cache` provides inert cache storage primitives missing from the resource and execution APIs. It never downloads URLs, evaluates sources, or grants execution/write authority. All routes retain local peer/Host/Origin or desktop-token guards, JSON mutation media types and DSH admission.
| Method | Path | Result / input |
| --- | --- | --- |
| GET | `/graphs` | `{ok:true,value:{graphs,sources}}`; metadata and unique source bytes, each source once |
| GET | `/graphs?metadata=1` | only stored graph metadata `{ok:true,value:{graphs}}`, with no source bytes |
| GET | `/graphs?owner=` | `{ok:true,value:{generation,pending?,graph?}}`; hydrated owner graph; missing generation is 0 |
| POST | `/graphs?owner=` | `{}`; advances generation, sets pending and retains previous references; returns generation |
| PUT | `/graphs?owner=` | `{generation,graph}`; publishes only if generation is current and pending; returns boolean |
| DELETE | `/graphs?owner=` | releases references and retains a newer source-free generation tombstone; returns generation |
| POST | `/graphs/import?owner=` | legacy `{generation,pending?,graph?}`; imports only if no Host record exists; returns boolean |
| GET | `/sources?url=` | newest referenced exact URL version: `{content,contentDigest,downloadedAt}` or null |
| GET | `/opening` | fixed inert snapshot `{generation,content}`; absent content is null |
| PUT | `/opening` | `{content,onlyMissing?}`; exact registered URL/hash/size, optional create-only legacy import; returns boolean |
| DELETE | `/opening` | releases fixed snapshot and retains its generation tombstone |
Sources are UTF-8 files under `/rendering-cache/sources/`, keyed by exact URL and SHA-256; `index.json` stores generations and references. Publication checks the entire environment’s 512-source / 64 MiB physical budget (including fixed opening data), 8 MiB per source, 4096 owner records, 2 MiB graph metadata and 16 MiB total index metadata. New bytes are written atomically before the index; obsolete bytes are collected after publication. Reads verify source size and digest. Concurrent requests to one Host use generation CAS; a snapshot is a read result, not an execution lease. Browser downloads retain CORS, no credentials and no redirects. Browser migration preserves originals, retries failed imports and never overwrites Host records or tombstones. Cards cannot access these storage APIs. Execution bindings and script selection remain independent.