Skip to content

v0.0.24: RPG plugin suite, Emberback world, LLM concurrency gate & scoped runtime retry - #39

Merged
ackness merged 13 commits into
mainfrom
v0.0.24-dev
Aug 1, 2026
Merged

v0.0.24: RPG plugin suite, Emberback world, LLM concurrency gate & scoped runtime retry#39
ackness merged 13 commits into
mainfrom
v0.0.24-dev

Conversation

@ackness

@ackness ackness commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

This cycle fills the RPG gameplay gap in the plugin ecosystem, ships a showcase world built for it, and hardens the runtime around slow-provider reality (concurrency gating, honest failure reporting, seeded per-runtime retry).

New gameplay plugins (4)

  • dice-check — zero-LLM check layer: a pre-turn roller injects a pre-rolled d20 pool into the narrative engines; risky actions resolve against it (die + character attribute vs DC) and land as ONE batched check.resolved receipt per turn (emit-event dedupes by topic — the batch payload is a contract, not an optimization). Receipts render as a 🎲 message block + right-panel log.
  • core-quest / affinity / inventory — post-turn extraction agents on the codex template, each with an entry-registered batch tool, world-importable dataSchemas, a right panel, and per-turn message blocks. Inventory additionally has a player-side item-op RPC (equip / unequip / drop buttons on the panel; consumption deliberately stays in the story loop).
  • Both narrative engines gain the <check-results> inject + an action-check prompt section, degrading cleanly when dice-check is absent. player-identity is archived to plugins/_archive/ (duty long since moved to the character card).

New world

  • Emberback (鳌背孤城·烬海纪) — the last great turtle carries the last city across an ember sea. Five 0-5 check attributes feed dice modifiers; worldData seeds 3 quests / 6 items / 3 NPC affinity records (Ajv-validated against plugin schemas), plus a 3-NPC portrait-backed main cast, 6 living-world rules, and 2 world memory blocks.

Runtime hardening

  • LLM concurrency gate — process-wide FIFO semaphore (COVEL_LLM_MAX_CONCURRENT, default 4) so a 7-agent post-turn fan-out no longer congests slow providers into blanket call-timeouts. Queue waits extend deadlines end-to-end (retry loop AND the agent tool loop via onQueueWait), verified live: a tracker that died at 2m29s queueing now completes in 6.3s; a full 11-runtime turn lands 11/11.
  • Terminal-event funnel — returned agent failures (timeout-without-output, requireToolUse unmet, tool-failed, schema short-circuits) now always emit runtime.failed; chips no longer spin forever on a failure the DB recorded.
  • Scoped runtime retry with recorded-turn seedingretry_runtime + retryFromTurnId replays a failed runtime against the ORIGINAL turn's persisted outputs (a bare manual trigger resolves injects empty). Seeds are context, not products: dropped before event fan-out (no event replay) and never re-persisted. Failed chips get a working ↻ button that keeps the narrative intact.
  • fix(web): chat auto-scroll is container-scoped (scrollIntoView on a sentinel could scroll the document and shove the whole app shell off-screen).

Docs & tooling

  • Registry/tool/panel/world-data docs synced; README (en + zh) updated to 26 bundled plugins / 3 flagship worlds; create-plugin & create-world skills fold in this cycle's field lessons (batch-event rule, bundled-plugin integration gates, world.yaml I18nText gate, RPG world recipe).

Validation

  • Full workspace pnpm lint · check:i18n · deps:check
  • Suites: runtime 848 · server 711 · web 497 · new plugins 84+ (incl. item-op, llm-slots, queue-wait propagation)
  • Bundled-set goldens updated (narrator inject list, post-turn parallel level, dataSchemas Ajv registry)
  • Live E2E on a real session: world-enter → character form → 3 player turns → 11/11 runtimes green with full terminal-event coverage; dice receipts (incl. a natural-20 crit and a failed shell-sense check) woven into narration; scoped retry of a timed-out core-quest succeeded with a real tool call
  • Desktop (pnpm build:electron) smoke — not run this cycle
  • Multi-provider check with a faster plugin slot (current test provider runs 25-90s/call under load)

Notes for reviewers

  • check.resolved batching and the retry-seed lifecycle (inject-only, dropped pre-fan-out) are the two easiest places to regress — both documented inline and in docs/reference/.
  • Known deferred items: dedicated unit test for executor retry-seeding (live-verified), keyed turn_results getter (full-scan noted with ponytail comments), tsx-watch not restarting on packages/ edits (observed; root cause pending).

ackness added 13 commits August 1, 2026 17:46
Four bundled plugins filling the RPG gameplay gap:

- dice-check: zero-LLM check layer — a pre-turn roller injects a pre-rolled
  d20 pool (<check-results>) into the narrative engines; they resolve risky
  actions against it and emit ONE batched check.resolved receipt per turn
  (emit-event dedupes by topic per turn, so the payload carries a checks[]
  array); an event recorder persists receipts for the message block and the
  right-panel log.
- core-quest / affinity / inventory: post-turn extraction agents on the codex
  template (dual-engine narrativeOutput inject + plugin-data state inject,
  needs narrative-engine), each with an entry-registered batch tool
  (upsert-quests / update-affinity / update-inventory), world-importable
  dataSchemas, a right panel, and a per-turn message block.

Both narrative engines gain the <check-results> inject and an action-check
prompt section (absent plugin degrades to normal narration). Bundled-set
goldens updated (narrator inject list, post-turn parallel level, dataSchemas
Ajv registry). Docs: plugins.md registry entries, tools.md rows, ui-panels.md
panels, world-data.md RPG seed namespaces; the planned-plugins table now only
lists combat.
鳌背孤城・烬海纪 — the last great turtle carries the last city across an
ember sea. Built around the four new RPG plugins: five 0-5 check attributes
(sinew/deft/shellSense/nerve/tongue) feed dice-check modifiers; worldData
seeds 3 quests, 6 items (incl. scale-coin currency), and 3 NPC affinity
records, all Ajv-validated against the plugin schemas and name-aligned with
the lore. Adds a 3-NPC main cast (blueprint import with instantiation),
6 living-world rules (1 constant + 5 keyword-triggered), 2 world memory
blocks (descent_log / turtle_state), portrait media bound by sha256 to the
cast ids, and the world-select visual.
Its duty moved to the character card long ago: voice/persona is set at
creation via world characterAttributes and injected as {{ player.character }};
the plugin had no UI, no preset membership, and an always-empty
persona-provider resolution. Moved to plugins/_archive/ (not loaded —
discovery only scans plugins/<dir>/PLUGIN.md, and the workspace glob
plugins/* skips nested dirs), same convention as worlds/_archive/.

The persona-provider capability hook and the shared player-identity types
stay — third-party plugins can still provide a persona, and the framework
degrades gracefully without one. Disk-loading test fixtures drop the id;
doc examples now point at affinity instead.
useAutoScroll followed new content with scrollIntoView on a bottom sentinel.
That scrolls EVERY scrollable ancestor — including the document — so any
transient page overflow while a session loads (layout settling, a pending
interaction form) shoved the whole 100vh app shell off-screen and nothing
ever scrolled it back: input bar at the top, void below.

All three follow paths (new messages, streaming deltas, jump-to-latest) now
scroll the tracked viewport directly via scrollTo, which cannot touch
ancestors by construction. The bottom sentinel leaves the hook API; the DOM
node stays for the parent-owned messagesEndRef.
The post-turn stage now fans out up to seven agent runtimes at once; firing
all their LLM calls concurrently congested slower providers until every call
timed out (observed live: guide burned its whole 2m deadline queueing at the
provider without a single attempt).

Both retry loops acquire a process-wide slot per attempt
(COVEL_LLM_MAX_CONCURRENT, default 4, 0 disables). Time spent queued extends
the runtime deadline — the gate's cost, not the runtime's budget — and
streaming calls hold their slot for the whole stream. Release is idempotent
and wakes exactly one FIFO waiter.
Four agent-runtime failure paths (deadline without content, requireToolUse
unmet, tool-failed, schema/prose short-circuits) returned a failed
RuntimeResult without any terminal event — the execution strip never learned
the runtime failed and its chip spun forever (observed live: 3 of 4 failures
in one turn invisible in the SSE stream while the DB artifact recorded them).

finalizeFailure is the funnel every returned failure routes through, so the
emission lives there; the two inline emits on the schema paths go (their
short-circuit results re-enter the funnel and would double-fire). Thrown
failures keep the caller's catch-side emission.
A bare manual trigger resolves input.inject and needs empty, so retrying a
failed post-turn agent used to burn tokens doing nothing — it never saw the
narrative it was meant to extract from.

manualTrigger.retrySeedResults carries the source turn's persisted runtime
outputs; the executor seeds completedResults with them (minus the target) so
inject/needs resolve against the original narrative. Seeds are context, not
products: they are dropped before the event fan-out — runEventChain collects
output.events from every completedResults entry, and live seeds would replay
the source turn's events (scene.set, receipts, generation requests) on every
retry — and therefore never reach the finalizer or the persisted artifact.

Entry points: actions retry_runtime gains payload.retryFromTurnId (falls back
to the latest player-origin artifact) and plugin-rpc's runtimeId branch gains
retryFromTurnId (404 on unknown turn). The execution strip's failed-chip
retry button now sends its turn id down this path and no longer wipes the
turn's narrative messages — that stays whole-turn-retry semantics.

Verified live: retrying a call-timeout core-quest succeeded in 20s with a
real upsert-quests call; artifact committed as origin=manual.
…te-world

create-plugin: emit-event's per-turn topic dedup is now a design rule (multi
receipt contracts must batch their payload — dice-check is the reference);
new bundled-plugin section covers the four integration gates the skill
stopped short of (pnpm install before filter tests, the three bundled-set
goldens that plain lint cannot catch, check:plugins, the doc-sync table).

create-world: the 'write everything in the user's language' line actively
produced 12 i18n violations — world.yaml display fields must be {zh,en}
objects and the gate scans worlds/*/world.yaml; validation table gains the
Ajv per-record seed check (L1b only validates descriptor shape) and the i18n
gate; new RPG-world recipe points at emberback; doc examples reference
affinity instead of the archived player-identity.
The inventory ledger was LLM-writable only; the player had no direct hands
on their own loadout. An entry-registered `item-op` RPC action now handles
equip / unequip / drop, and the panel grows per-item buttons wired through
invokePluginAction with the item key.

Scope is deliberately loadout-only: equipping is a player configuration
choice, but USING an item has narrative consequences — a silent quantity
button would bypass the narrator, so consumption stays in the story loop.
Drop writes the same tombstone shape as the tool's remove-to-zero, keeping
one removal model on both sides.

Review's live probe caught the handler passing a partial record to the raw
store's setPluginData (NOT NULL crash on every click — the unit mocks hid
it); it now spreads the loaded row so the upsert keeps identity and
timestamps. Verified with a live unequip/equip round trip.
The concurrency gate's queue exemption only stretched the retry loop's own
local deadline. The agent TOOL loop still measured queue time against its
original budget, so a multi-step agent that queued for minutes reached its
final produce-output step with the loop deadline already spent and died
with 'timed out while waiting for final output after tool execution' —
observed live on character-tracker (2m29s failure), whose calls all
succeeded but whose closer never got to run.

The retry loops now report each slot wait through an onQueueWait callback
and the tool loop shifts its (now mutable) deadline by it; the loop already
returns the deadline, so the runtime's timeout-vs-max_steps classification
follows automatically. Verified live: the same tracker completes in 6.3s
and a full 11-runtime turn lands 11/11 with complete terminal-event
coverage.
Highlights gain the RPG-mechanics bullet (dice checks, quest log,
player-managed inventory, affinity meters); flagship worlds go two → three
with the Emberback entry; the bundled-agent count moves 23 → 26
(player-identity archived, four RPG plugins added). zh-CN kept in sync.
…s/_archive

The plugin-loader package keeps its own dataSchemas golden over the bundled
set — the fourth such list (after runtime x2 and server), and the one CI
caught because the local battery never ran @covel/plugin-loader. Adds the
three new RPG namespaces.

release-preflight treated plugins/_archive as a plugin and blocked on its
missing package.json; underscore-prefixed dirs are non-plugin conventions
(discovery and the workspace glob both skip them) so the scan does too.
Full-workspace pnpm test now green (41 suites).
Bump all workspace packages 0.0.23 → 0.0.24, add the 0.0.24 changelog
section, and move the README version badges/release lines (en + zh).
@ackness
ackness merged commit fd9245c into main Aug 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant