v0.0.24: RPG plugin suite, Emberback world, LLM concurrency gate & scoped runtime retry - #39
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
check.resolvedreceipt 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.dataSchemas, a right panel, and per-turn message blocks. Inventory additionally has a player-sideitem-opRPC (equip / unequip / drop buttons on the panel; consumption deliberately stays in the story loop).<check-results>inject + an action-check prompt section, degrading cleanly when dice-check is absent.player-identityis archived toplugins/_archive/(duty long since moved to the character card).New world
Runtime hardening
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 viaonQueueWait), verified live: a tracker that died at 2m29s queueing now completes in 6.3s; a full 11-runtime turn lands 11/11.runtime.failed; chips no longer spin forever on a failure the DB recorded.retry_runtime+retryFromTurnIdreplays 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.scrollIntoViewon a sentinel could scroll the document and shove the whole app shell off-screen).Docs & tooling
Validation
pnpm lint·check:i18n·deps:checkpnpm build:electron) smoke — not run this cyclepluginslot (current test provider runs 25-90s/call under load)Notes for reviewers
check.resolvedbatching and the retry-seed lifecycle (inject-only, dropped pre-fan-out) are the two easiest places to regress — both documented inline and indocs/reference/.turn_resultsgetter (full-scan noted with ponytail comments), tsx-watch not restarting onpackages/edits (observed; root cause pending).