Skip to content

v0.0.16 (WIP): capability-driven context budgets, cost estimation, legacy cleanup, docs sync#23

Merged
ackness merged 10 commits into
mainfrom
v0.0.16-dev
Jul 17, 2026
Merged

v0.0.16 (WIP): capability-driven context budgets, cost estimation, legacy cleanup, docs sync#23
ackness merged 10 commits into
mainfrom
v0.0.16-dev

Conversation

@ackness

@ackness ackness commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Draft — v0.0.16 scope is still in planning; this PR tracks work landed so far and is NOT part of the release flow (no version bump / tag yet).

What changed

Long-context stability + cost visibility (d6a8977d)

  • Compaction threshold and the prompt hard-prune budget now resolve live from the narrative slot's model capability (llm.toml hot-reload aware); COVEL_COMPACTOR_CONTEXT_WINDOW becomes an optional override (fallback 32768).
  • New CJK-aware estimateTokens replaces the chars/4 heuristic that undercounted Chinese text ~3x.
  • applyBudget hard prune activated as the last line of defense (estimator + getter-based budget injected into turn-executor deps).
  • Debug Cost panel: per-model USD estimation via model-db pricing (model recovered by pairing llm.responded with the preceding llm.calling).
  • Bundled LiteLLM model DB refreshed 2597 → 2967 models (gitignored asset, ships via packaging).

Dead-code / legacy cleanup (f275a3d1, 7bcfd960, −2600 lines)

  • Removed verified-dead code across packages and apps: memory's never-called second compactor, unwired Langfuse hook, createPlugin programmatic API, 18 superseded store row-mappers, 3 orphan shared type modules, web lorebook client + orphan API functions, createTestHarness, write-only blockSchemas chain (incl. GET /api/block-schemas), tauri config residue, LEGACY_EVENTS fallback, never-read COVEL_ALLOWED_LLM_HOSTS.
  • Deprecated PLUGIN.md registration fields (tools.local/hooks/rpc/wires) given an explicit sunset: removal planned for v0.0.17.

Plugin contract fixes (391d0526)

  • .npmrc supply-chain guard added to all 20 bundled plugins; scene-prompts gains PLUGIN.en.md; haruka-academy migrated to pluginPolicy.

Docs synced to current architecture (7452f693)

  • 30+ verified drift fixes across flow/api/protocol/plugins/tools/ui-panels/glossary/CLAUDE.md (all backed by file:line checks; historical docs untouched).

Why

Compaction previously triggered against a fixed 32k window regardless of the actual model, Chinese sessions blew past real limits unnoticed, and users had no cost visibility. An 8-way audit then confirmed substantial dead/legacy code and doc drift accumulated across recent iterations.

How it was validated

  • Full-workspace pnpm lint (21/21), pnpm test (40/40 packages), format:check, deps:check, check:i18n all green.
  • Every deletion grep-verified zero-reference with both *.ts and *.js (plugins are plain JS), plus an adversarial review pass over the diff.
  • New unit tests: CJK estimator, budget resolution priority, cost-panel model pairing.
  • Secret/local-path scan over the full diff: clean.

Known risks / follow-ups

  • v0.0.16 scope not final — more work lands on this branch before release.
  • Backlog (recorded): Langfuse observability as a proper feature (the deleted hook was never wired); compat-layer removal execution in v0.0.17; web one-shot migration shims retire at 1.0.
  • Budget window uses the global default slot capability, not per-session runtime_model_overrides (noted in code).

ackness added 8 commits July 17, 2026 11:03
…lity, add cost estimation

- Compactor context window now resolves live from the narrative slot's
  model capability (llm.toml hot-reload aware); COVEL_COMPACTOR_CONTEXT_WINDOW
  becomes an optional explicit override (fallback 32768).
- New CJK-aware estimateTokens replaces the chars/4 heuristic that
  undercounted Chinese text ~3x.
- Activate the applyBudget hard prune as last line of defense: estimator +
  getter-based contextBudget injected into turn-executor deps.
- Debug cost panel: per-model USD estimation via model-db pricing; model
  recovered by pairing llm.responded with the preceding llm.calling.
- ModelCapabilityInfo gains the flat per-M-token price fields actually
  returned by /api/model-db/lookup.

Riders on shared files: memorySystem module-setter typing cleanup
(actions.ts), withSessionLock ghost-comment reword (env.d.ts), dead
searchModelDb removal (llm.ts), orphaned lorebook i18n keys removal
(locales), doc-sync lines in CLAUDE.md / env-registry.md.
Deletions verified zero-reference (.ts and .js cross-checked; plugins are
plain JS):

- memory: unused compactor chain (production compaction lives in
  @covel/context maybeCompact) incl. CompactionConfig/Result types
- ai-provider: never-wired Langfuse trace hook + LANGFUSE_* env entries
- store: 18 superseded row-mapper re-exports (both SQL backends),
  mediaObjectKey, dead ALL_TABLE_NAMES re-export
- shared: orphan plugin type modules (character-presence, branch-reply,
  living-world-rules)
- runtime: HookPipelineRun, backward-compat re-export block in index.ts,
  collectEventsFrom export demoted to private
- create: createPlugin chain (CLI scaffold is an independent script);
  createWorld path kept
- plugin-test-utils: createTestHarness retired (documented rejection in
  scene-stage integration test); unused deps dropped, lockfile synced
- web: lorebook client (backend routes stay), schema-block-renderer,
  searchModelDb, fetchPluginDocs, getPluginData, syncSessionDimensions,
  legacy fetchTraceEvents, browser media-store, LEGACY_EVENTS fallback,
  LEGACY_SLOTS renamed to DEFAULT_FALLBACK_SLOTS
- server: pathExists, ui-spec-schema dead exports demoted,
  loadWorldPackages export demoted, withSessionLock ghost comments fixed
- root configs: stale apps/desktop-tauri entries
- docs: plugin-testing guide rewritten around the patterns actually in
  use; flow/authoring guides and create-plugin skill references synced;
  CHANGELOG CI wording matched to the real workflow
- Add the supply-chain .npmrc (minimum-release-age) to all 20 bundled
  plugins, matching the plugin-authoring appendix; scaffold already
  copies it from templates via dotfile-inclusive copyDir.
- Add scene-prompts PLUGIN.en.md so English sessions get an English
  agent prompt (was the only CJK-bodied schedulable agent without one).
- Migrate haruka-academy world.yaml from deprecated top-level
  required/recommended/excludedPlugins to pluginPolicy (fold shim kept
  for third-party worlds).
…ry, set compat-layer sunset

- Remove the blockSchemas dead chain end to end: the registry was written
  at boot but never read after message rendering moved to json-render.
  Drops block-renderer.tsx, boot-time fetch/set, fetchBlockSchemas client,
  GET /api/block-schemas route, BlockSchemaDeclaration/Meta shared types,
  and the api.md row.
- Remove the never-read COVEL_ALLOWED_LLM_HOSTS registry entry; the SSRF
  guard is open by design (CLAUDE.md / env-registry wording updated).
- Give the deprecated PLUGIN.md registration fields (tools.local / hooks /
  rpc / wires) an explicit sunset: planned removal in v0.0.17 (deprecation
  note, boot warning, plugins.md).
Cluster-by-cluster verification of docs/ against the codebase (every fix
backed by a file:line check):

- flow.md: model-db count 2967, createGateway factory names, full
  11-type proposal enum, compactor + applyBudget window resolution,
  plugin-test-utils surface, slot examples aligned with slots.md
- api.md: drop deleted plugin-data-contract route; add missing
  DELETE /api/worlds/:id, /api/plugin-flows, media-token, install
  endpoints; rewrite event-type tables around the CovelEvent union
- protocol.md: fix stale file/line anchors, add proposalType to hook
  trace payloads
- plugins.md/tools.md: fix world-init ui spec name, branch-reply slot
  column, chat-mode-narrator requires, player-init runtime shape
  (agent + guard, not function), approval policy path; mojibake repair
- ui-panels/ui-components: drop fixed Lorebook tab claim, add missing
  character-presence and scene-cast panels, component count 48
- CLAUDE.md: 27 store tables (with real pointer), app.ts size, ~48
  component catalog
- glossary.md: split pluginType (core-plugin/plugin) from trust tier
  (builtin/official/community) - two independent axes
- README/guides: verified, minor sync only

Historical docs (CHANGELOG, explorations, superpowers, audits)
deliberately untouched.
Explore/verify workflow audit (24 confirmed findings); this lands all
small, safe fixes:

- dead code: unused @covel/context re-exports in runtime barrel,
  cloneVectorRows, 8 unused ui-* CSS classes (incl. paper theme ref)
- stale docs: remove CLAUDE.md section for the deleted LLM replay
  cache, correct TRUSTED_PROXY_IPS status in env-registry, retag
  shipped S4-T4.c TTL sweep from TODO to spec, fix stale comments
- logging: settings REST backend now surfaces failed PUTs (res.ok),
  keep stacks in turn-executor warn paths, add [shutdown] prefixes,
  log Data Explorer catch paths and model-db refresh failures
- consistency: shared readJsonBody helper in api-error.ts; 13 routes
  now return 400 invalid_json_body instead of 500 on malformed JSON
- perf: plugin-data-store snapshots scoped per (pluginId, namespace)
  so unrelated plugin-data writes no longer re-render every panel
… from registry

Two verified high-impact items from the six-dimension audit:

1. Bounded per-turn message reads. loadTurnSessionState no longer
   full-reads listTurnMessages every turn (the log only ever grows —
   compaction tags rows but never deletes). New DataStore surface:
   - listUncompactedTurnMessages(sessionId): the raw suffix the
     compactor and prompt builder actually operate on (the compacted
     prefix is represented by session summaries)
   - getTurnMessageStats(sessionId): grouped-count aggregate for
     turnNumber / per-runtime trigger counts (GROUP BY on SQL backends
     via a new SelectOpts.groupBy; shared JS fallback for memory/idb)
   buildMessageHistoryWithSummaries now emits summaries not referenced
   by any history row up front (chronological), so uncompacted-suffix
   histories keep their compacted context; maybeCompact counts ALL
   session summaries for its token estimate for the same reason.
   Full-history callers are byte-identical in both cases.

2. IDB deleteSession cascade is now derived from SESSION_SCOPED_TABLES
   (new explicit idbStore field), matching the SQL/Memory backends —
   a newly registered session table can no longer silently leak rows
   on the browser backend. Index-vs-filter deletion is auto-detected
   from the store's own indexNames; the IDB-only
   state_snapshot_metadata sidecar stays as one explicit residual.

Validation: store contract suite extended for both new methods and run
against memory/sqlite/idb plus a real PostgreSQL container (152/152 —
covers PG's string count(*) normalisation); runtime read-dedup test
updated to pin the new two-read pattern; full turbo lint 21/21.
…al, settings probe, chat window cap)

Four verified backlog items, per owner triage:

- perf(memory): recall ingestion no longer full-reads + sorts the whole
  turn-message log every turn. New DataStore.listTurnMessagesAfter
  (forward keyset on (createdAt, id), SQL tuple predicate + shared JS
  fallback, contract-tested incl. same-createdAt tie-break) reads just
  the batch window past the persisted cursor. Also fixes a latent
  stall: a run of empty-content rows now advances the cursor instead
  of pinning it forever, and cursor ordering now matches the store's
  byte-order keyset exactly.

- refactor(runtime,server,context): remove two zero-caller extension
  seams. findToolClient (structurally broken for any non-InMemory
  client) is gone from ToolExecutorConfig; tool resolution is the
  plain findTool path. The getConfigFn / ctx.config / {{ config.* }}
  chain (always {} in production, undocumented) is deleted end to end,
  plus stale comment references ({{ config.worldEntries }} et al).

- fix(web): settings backend selection now honours the REST-desktop
  tier. probeDesktopMode() runs BEFORE initSettings() and the backend
  picks via isDesktopApp(), so self-host setups without the Electron
  IPC bridge persist settings to ~/.covel/settings.json instead of
  silently falling back to localStorage.

- feat(web): live chat messages are capped in memory (configurable
  ui.chatMessageWindow setting, default 2000, min 200). The cap drops
  oldest rows on live append only and advances olderMessagesCursor to
  the new window edge so scroll-up re-fetches exactly the dropped
  range; streaming placeholders are never dropped and explicit
  history loads never cap.

Validation: full workspace tests 40/40 tasks, turbo lint 21/21,
prettier + oxlint clean; new contract tests ran on memory/sqlite/idb.
@ackness

ackness commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

Appended the 2026-07-17 six-dimension audit series (3 commits):

  • 3a20d2da refactor: apply verified six-dimension audit fixes — 24 adversarially-verified findings: dead code (runtime barrel re-exports, cloneVectorRows, 8 dead CSS classes), stale docs (removed the long-deleted LLM replay cache section from CLAUDE.md, env-registry corrections, TODO→spec retags), logging gaps (settings REST save failures now surface via res.ok, stacks kept in turn-executor warn paths, [shutdown] prefixes), shared readJsonBody so ~13 routes return 400 instead of 500 on malformed JSON, and per-(pluginId, namespace) plugin-data snapshots so unrelated writes no longer re-render every panel.

  • f07bedec perf(store,runtime): bound per-turn history reads; derive IDB cascade from registry — new DataStore surface (listUncompactedTurnMessages + getTurnMessageStats grouped aggregate) removes the per-turn full-log read (the log only grows; compaction never deletes rows); IDB deleteSession cascade now derives from SESSION_SCOPED_TABLES (new idbStore field), closing the silent-orphan gap for newly registered tables. Contract suite extended and run against memory/sqlite/idb + a real PostgreSQL container (152/152).

  • 39289eda refactor: land second audit-backlog batch — bounded keyset recall ingestion (listTurnMessagesAfter, also fixes an empty-content cursor stall), removal of the zero-caller findToolClient / getConfigFn seams end to end, REST-desktop settings persistence (probe before init, isDesktopApp()-based backend choice), and a configurable in-memory chat window cap (ui.chatMessageWindow, default 2000) that advances the scroll-up cursor so dropped rows stay re-fetchable.

Validation: full workspace tests 40/40 turbo tasks, lint 21/21, prettier/oxlint/i18n clean.

ackness added 2 commits July 17, 2026 15:59
…l discovery)

Runtimes with large tool whitelists can now declare tools.defer
(true = whole whitelist, string[] = listed names). Deferred tools stay
registered and authorized but are withheld from the initial LLM tool
list; the framework injects a search-tools definition instead, whose
description advertises the pool size and owner. When the LLM calls it,
the agent loop intercepts the call (suspend/runtime-done sentinel
pattern — never dispatched to the ToolExecutor), ranks the deferred
pool with BM25, and grows the working tool surface so matches are
directly callable from the next step for the rest of the turn.

Design follows openai/codex's tool_search (ToolExposure::Deferred +
BM25 over name/description/parameter-schema text, default limit 8):

- zero-dep BM25 (k1=1.2, b=0.75) in @covel/tools; CJK text tokenizes
  into character bigrams so Chinese queries match non-adjacent words
  ('解锁图鉴' → '解锁…图鉴条目'), with substring fallback
- corpus per tool = name + description + parameter-schema field
  names/descriptions (parameterSchemaSearchText)
- search-tools is framework-injected only: not registered in the tool
  map, not declarable in tools.builtin; searchable pool is exactly the
  runtime's own declared whitelist resolved through getToolInfo, so
  the builtin/local access boundary is inherited unchanged
- defer entries outside the whitelist are ignored — deferring can
  never grant access to an undeclared tool
- searches are traced as normal toolCalls records

No-defer runtimes are byte-identical to before (regression-pinned).

Docs: tools.md (registry row + search-tools contract), plugins.md
(tools.defer field), plugin-authoring-advanced.md (usage guidance).

Validation: 14 new tests (BM25 ranking/CJK/limits ×9, agent-loop
integration ×5); full workspace tests 40/40 tasks, lint 21/21,
prettier + oxlint clean.
…er path

Provider connectivity test (and every real LLM call) failed with
"SSRF policy rejected ...198.18.x.x" on machines running a TUN proxy:
Clash/mihomo/sing-box/Surge fake-IP mode maps every external hostname
into 198.18.0.0/15 and routes by SNI, and the DNS-pinning guard
rejected those answers as non-public. LAN endpoints (Ollama at
192.168.x.x) hit the same wall.

The self tier is already single-user local play (loopback-bound,
owner/operator tokens no-op), so the core provider path (the user's
own configured baseUrl via postJson/getJson) now accepts any resolver
answer for a hostname; the socket stays pinned to it, preserving the
anti-rebinding guarantee. The exemption is deliberately narrow:

- plugin ctx.http (third-party code) stays strict — no local-network
  probing even on desktop
- IP-literal URLs get no exemption (url-safety blocks private literals)
- hosted tiers (demo/commercial) keep the strict public-only policy

Verified end-to-end against api.deepseek.com through the real
connect-pinned dispatcher (reaches the host, 401 on the dummy key).

Tests: request-dns-safety split into hosted-tier-strict and
self-tier-core-accept + ctx.http-still-strict cases; ai-provider 239
pass; turbo lint 21/21.
@ackness
ackness marked this pull request as ready for review July 17, 2026 08:27
@ackness
ackness merged commit aff9c9d into main Jul 17, 2026
2 checks 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