Skip to content

feat(story): let a story span scenes and end when its arc resolves - #136

Open
spuentesp wants to merge 5 commits into
masterfrom
feat/loops-followup
Open

spuentesp wants to merge 5 commits into
masterfrom
feat/loops-followup

Conversation

@spuentesp

@spuentesp spuentesp commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Landing the two pieces of pending loop work that were reviewed and are worth
keeping, rebased onto master (the original branch was 39 commits behind and
conflicted in three loop files).

Ingestion (2289ca7, cherry-picked unchanged — it applies cleanly)

  • Capture the ingestion job id through an explicit on_job_created callback
    instead of monkey-patching mongodb_create_ingestion_job. The patch was
    module-level, so concurrent ingests could claim each other's job id.
  • Extract each PDF page once per document instead of once per bookmark span,
    and keep the front matter before the first bookmark as its own section
    (those pages used to be dropped entirely).
  • Classify a chunk from the deepest heading that carries a known tag, falling
    back through its ancestors.
  • Record the section's page range in the chunk metadata, and add
    scripts/assess_ingestion_corpus.py to measure PDF coverage without an LLM.

Story arc (b30de84, a3e4d05)

A finished scene now advances the story instead of ending it, and the story
ends when its arc resolves.

The arc runs rising action → climax → falling action → resolution, and the
new scene_budget on StoryState (5 scenes by default) decides how long each
phase lasts. The old heuristic could never leave rising_action: tension
saturated below the climax threshold and no plot thread was ever closed, so
story_complete could never become true — the story had no way to end.

The story state is checkpointed after every scene, and start_or_resume()
returns that checkpoint instead of restarting the campaign, so an interrupted
campaign keeps its scenes, its arc position and its in-game time.

Counting a finished scene now happens in one place: the caller counts the
scene it just played and the transition only opens the next one. It used to
count it in both, so scenes_completed grew twice per scene.

Scene and conversation fixes (db68b96, b9cf7e9)

  • finalize() closes the scene without resolving or narrating an empty turn.
  • In a group conversation each NPC hears the replies the others already gave
    this round.

Verification

  • packages/agents, packages/data-layer, packages/ui/backend and the
    repo-root tests/ suite
  • ruff check, ruff format, check_layer_dependencies.py
  • mypy on packages/agents/src — no new errors (the 12 that remain are
    pre-existing, in files this PR does not touch)
  • New: packages/agents/tests/test_story_arc_contract.py, plus PDF page-bound
    tests in packages/data-layer/tests/test_db/test_ingest_tools.py

Mutation testing runs locally, not in CI. On the changed lines, every mutant
the code can produce is killed by the tests — 42 of 42, checked one at a time
(cosmic-ray needs longer than the budget on these modules).

Three pre-existing notes, all reproduced on master without these changes:

  • typecheck fails on the strict-mypy backlog (the job is
    continue-on-error).
  • unit-tests reaches 96% green and then hits the job's 20-minute timeout;
    master's run is cancelled the same way.
  • mutation-diff exceeds the 600s per-module budget on indexer/agent.py,
    the same way every recent PR does.

Sebastian added 5 commits November 7, 2026 01:14
A finished scene now advances the story instead of ending it. The arc runs
rising action, climax, falling action, resolution, and the story's new
scene_budget (5 scenes by default) decides how long each phase lasts — so a
story always reaches its resolution. The old heuristic could never leave
rising action: tension saturated below the climax threshold, no plot thread
was ever closed, so the arc could never report the story complete.

The story state is checkpointed after every scene, and start_or_resume()
returns that checkpoint instead of restarting the campaign, so an
interrupted campaign keeps its scenes, its arc position and its in-game
time.

Counting a finished scene happens in one place now: the caller counts the
scene it just played and the transition only opens the next one. It used to
count it too, so scenes_completed grew twice per scene.
finalize() sets finalize_requested and the graph routes straight from
load_context to complete_current_scene, so a scene the player quits out of
is not resolved and narrated as an empty turn first.
In a group conversation every NPC now hears what the others said before it
answers, so the group answers as a group instead of in isolation.
The arc's phase now comes from an index into the four phases rather than a
chain of comparisons against the scene count, which makes the phases — and
the tension each one carries — one thing to read and one thing to test.

The tests pin the whole curve: which phase each scene lands in, the tension
it carries, and when the plot threads open and are paid off. The PDF tests
pin the exact pages a section claims and the depth of the front matter.
Every mutant the changed lines can produce is killed by these tests.
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