Skip to content

Story summary regeneration recovery

Episode requests instruct the model to finish the entire JSON object within 1,250 tokens. The provider output cutoff is 1,500 tokens. The final merge keeps its existing 2,400-token cutoff. Prompt compliance is not guaranteed, and the buffer does not resolve upstream refusals or empty responses.

The regular episode prompt aims for 900 tokens with a 2–3 sentence narrative (150 tokens), at most eight facts across six lists (two per list, 40 tokens per item), and at most six keywords. Empty lists are allowed and repeated facts are omitted. The 1,250-token ceiling still includes every key and value. These are model instructions, not server-side truncation or a guarantee of compliance.

When an episode returns nonempty output with max_tokens, its transcript is split once into two Unicode-safe halves. Each half is attempted once through the existing provider path. A truncated half fails the run; there is no recursive split loop. All calls, including the existing refusal fallback, share the existing 32-attempt run budget. Truncated output is never accepted as a summary.

Recovery halves use a more compact prompt: aim for 600 tokens, one narrative sentence (60 tokens), at most four facts in total (one per list, 35 tokens per item), and at most four keywords. They keep the same 1,250-token ceiling and 1,500-token provider cutoff. Splitting the input alone was insufficient in the synthetic Chinese fixture: two half-sized requests still returned 1,500 tokens when they repeated the original 5–10 sentence and uncapped-list instructions.

Provider Terms of Service rejections also trigger the existing one-time DeepSeek V3.2 fallback. This includes OpenRouter's formatted error message, which previously escaped the summary's content-refusal check. A rejected fallback ends the attempt; ordinary authentication, permissions, and credit errors do not select another model. The displayed provider error no longer claims a fallback was attempted when the provider cannot establish that.

Successful episode results and split decisions are cached for one hour. Keys include user, session, selected model, full episode prompt, and output cutoff. Retries reuse completed siblings and halves. Cache failures degrade to uncached work and never discard otherwise valid generated output. Successful calls retain the existing billing behavior; partial successful work can be charged even if a later call fails.

The cache uses the actual normal or recovery prompt for each request. Updating the prompt invalidates older entries, and a compact half cannot reuse a normal episode result for the same transcript.

POST /api/sessions/:sessionId/summary/regenerate returns HTTP 202 with data.job. Its job states are queued, running, completed, and failed; phase, completed, and total describe episode/merge progress. Poll the existing summary GET endpoint for progress and the persisted result. The Session Memory panel polls every 1.5 seconds during active work, reconciles a lost POST receipt with GET, and disables conflicting summary buttons while the job is active.

Job admission uses Redis compare-and-set to share one active regeneration per user/session across replicas. New jobs fail closed if configured Redis is unavailable. Summary reads remain available from PostgreSQL during a Redis outage. Installations without Redis use a bounded in-process store, so receipt sharing and checkpoints there are instance-local; PostgreSQL claims still prevent simultaneous summary writes.

Workers heartbeat every ten seconds and receive an abort signal after ten minutes. A crashed worker is considered abandoned after 15 minutes 30 seconds without a heartbeat, allowing the existing 15-minute PostgreSQL claim to expire first. Each regeneration includes its job ID in the source hash, preventing a superseded worker from persisting against a replacement's claim. Background execution remains attached to the server process; a restart requires a retry after claim expiry rather than automatically resuming the job.

The existing oversized full-regeneration guard remains in effect. For a history beyond that guard, use incremental compression rather than rebuilding the entire history in one request.

Local verification

On main, the normal server test command does not remove credentials or isolate the database. Do not run database-using suites against a developer environment. In a clean, credential-free checkout with dependencies installed, run these focused tests from the repository root; they use pure functions and in-memory test doubles:

sh
pnpm --filter @yumina/server exec tsx --test src/lib/summary-episode-recovery.test.ts src/lib/summary-job-core.test.ts src/lib/story-summary-prompts.test.ts
pnpm build
pnpm typecheck

Broader release verification used a temporary harness that stripped credentials, disabled dotenv loading, blocked unmocked external fetches, and provisioned an in-memory PGlite schema for selected integration suites. That untracked harness is excluded from this release; the command above does not provide those isolation controls for other suites.

In a testing session, regenerate a summary and verify that the request returns a queued receipt, duplicate requests return the same job ID, progress survives reopening the panel, and the completed summary replaces the previous text. Injected truncation tests verify one-level splitting, terminal half failures, and reuse of successful work. No production database mutation is needed for this verification.

For the provider-refusal regression, mocked HTTP tests exercise the real OpenRouter formatter in streaming and non-streaming requests, then verify fallback success, terminal rejection, cancellation, and shared attempt accounting. In the testing UI, retry the failed synthetic session or use its fresh copy. Wait for saved summary text and a last-updated time; the "Summary started" toast alone confirms only job admission. A completed job that used the refusal fallback records DeepSeek as the summary model. A live provider may still reject the fallback, so successful mocked recovery does not guarantee acceptance of every live request.

For prompt quality, compare old and new prompts on the same synthetic full and half transcripts using the same model, temperature, reasoning setting, and cutoff. Record actual completion tokens, stop reason, JSON validity, and whether names, outstanding obligations, and the ending state were preserved. Also exercise all episodes and the final merge. Construction and recovery unit tests do not establish live model compliance; do not describe them as a successful live regeneration.

In testing QA run A, the eight episode outputs used 442, 471, 674, 1,132, 421, 486, 511, and 573 tokens; the merge used 2,137 tokens. The summary was saved with 45 messages compacted and 15 left raw. This records one successful testing run, not a guarantee for every model or proof that every story detail was preserved.