Beat dedupe misses same-moment duplicates and split moments, inflating the event log #588
Closed
opened 2026-09-10 05:50:11 +00:00 by claude-bot
·
6 comments
No Branch/Tag specified
main
feat/1328-fill-it-in
feat/1328-fill-frontend
feat/1328-fill-backend
feat/9-foundry-push
fix/9-foundry-live
feat/9-foundry-push-frontend
renovate/celery-5.x
feat/5-foundry-connect
feat/9-foundry-push-backend
feat/9-foundry-push-module
feat/5-foundry-frontend
feat/5-foundry-backend
feat/5-foundry-module
release/v4.8.0-rc1
docs/d341-convert-draft
feat/958-update-to-version
feat/958-convert-sheet
feat/958-pathbuilder-import-frontend
fix/1360-heading-order
feat/958-pathbuilder-import-backend
feat/11-weekly-digest
feat/1341-auto-update
docs/d332-superseded
chore/1345-names-cleanup
feat/1342-mentioned-pages
feat/1328-recipes
fix/1346-pgdg-key
perf/1247-shared-recording-stream
chore/132-gitleaks-allowlist
release/v4.7.1
spike/foundry-module
release/v4.7.1-rc1
docs/decisions-d332-d337
fix/bot-certifi
chore/release-4.7.0
chore/release-4.7.0-rc4
chore/deps-4.7.0
chore/release-4.7.0-rc3
fix/1280-quote-must-say-it
fix/1257-names-row-squeeze
chore/release-4.7.0-rc2
fix/1245-pg-upgrade-count
chore/release-4.7.0-rc1
feat/1257-names-in-prep
feat/1257-be
feat/1257-fe
feat/1257-rating-fixes
design/1328-fill-it-in
feat/1257-name-splits
fix/1326-redis-503
feat/1280-fact-suggestions
feat/1280-be
feat/1280-fe
design/1280-fact-suggestions
feat/1279-infobox
feat/1279-be
feat/1279-fe
feat/1321-shelf-copies
design/1279-infobox
feat/1243-table-find
feat/1257-who-rated
feat/1243-prep-drawer
feat/1243-sheet-pickers
design/1243-browse-in-place
feat/1315-hotwords-default
feat/1242-system-filters
feat/1257-more-name-styles
feat/506-leg3-hotwords
design/1242-system-filters
fix/1302-packs-polish
feat/1241-rules-library
design/1241-rules-library
feat/1245-postgres-18
integ/4.6.2
fix/1263-map-ui-2
fix/1263-map-server
fix/1263-map-ui-1
fix/1261-editor-link-menu
fix/1229-axe-4.14-name-mismatch
renovate/axllent-mailpit-1.x
renovate/eslint-monorepo
renovate/authlib-1.x
chore/release-4.6.0
chore/release-4.6.0-rc2
fix/460-rc2-screens
fix/460-rc2-server
fix/release-prerelease-exit
chore/release-4.6.0-rc1
feat/125-campaign-templates
feat/126-calendar
feat/1181-pictures-and-files
feat/1190-event-people
integ/4.6.0
feat/1191-line-of-the-night
feat/1200-starter-packs
chore/bot-multidict-and-playwright-group
fix/1202-this-week-answered-count
fix/122-124-leftovers
fix/1178-get-routes-that-write
fix/1203-journeys-check-names
design/125-campaign-templates
feat/126-calendar-engine
design/126-in-world-calendar
fix/1189-cited-stamp-in-range
fix/1189-v4-looser-check
fix/1189-richer-beats
fix/1113-export-bind-lists
fix/1154-release-notes-by-file
chore/release-4.5.0
chore/release-4.5.0-rc12
fix/1135-wiki-markup-as-text
fix/demo-usage-cost-in-window
chore/release-4.5.0-rc11
fix/1121-sheet-consistency
fix/1121-sheet-5e-terms-and-card-hp
copy/1122-busiest-pages
fix/1121-relevant-choices
fix/1121-wiki-create-npc-save-sheet-links
fix/1029-menu-stays-on-screen
docs/prototype-transcript-bin-wording
fix/1023-editor-save-row-back-scroll
fix/1020-search-exact-first
fix/1020-table-prep
fix/1020-shell-wordmark-footer-bell
fix/1020-wiki-article-party
fix/872-admin-kept-until-phone-tables
feat/l13-milestone-level-waiting
fix/1000-1001-bell-older-setup-context
fix/999-pronouns-in-prompts
fix/l19-other-class-feats
fix/1010-transcript-window-ordinary-confirm
docs/d254-d269-decisions
fix/702-member-search
fix/680-transcript-bin
fix/993-unidentified-prompts
fix/841-redis-timeouts
fix/990-binned-not-exported
fix/830-export-prep-shelf
fix/deps-audit-2026-10-01
fix/877-title-reads
fix/798-audio-received-at
build/lane11-17-j7
build/lane11-16-levelup
build/lane11-14-seams
build/lane11-14-seed
build/lane11-15-start
build/lane11-13-old-block-out
fix/980-review-r3
build/lane11-12-block
build/lane11-8-packs
fix/demo-seed-canonical-names
build/lane11-11-hot-half
docs/l11-decisions
build/lane11-7-convert
build/lane11-10-editing
fix/npm-audit-brace-expansion
build/lane11-9-sheet-page
fix/bot-pyjwt
build/lane11-6-content
fix/lane11-rank-names
build/lane11-2b-language
build/lane11-4-js-evaluator
build/lane11-3-server
build/lane11-5a-pf2e
build/lane11-5b-5e
feat/design-admin
build/lane11-2-evaluator
build/lane11-1-sheet-pieces
build/lane11-0-drawing
fix/943-quest-objectives-fk
fix/prepsheet-shelf-flake
fix/945-leftovers
build/demo-gm-copy
fix/901-email-landings
build/lane10-8-journeys
build/demo-seed-lanes-8-10
fix/887-gm-own-confirmation
build/lane9-5-j1-admin
fix/918-audience-click
fix/useasync-overtaken
ci/939-critical-path
test/937-backend-suite-speed
chore/claude-orchestration
fix/924-xp-gm-only
fix/session-stale-poll
docs/decisions-d239-d242
fix/930-pypdf
build/lane8-journeys
build/925-quest-editor
fix/894-private-page-pictures
build/lane8-p4-drafts
build/lane8-p7-map
build/lane8-p6-handouts-rules
fix/wiki-fixture-names
build/lane10-d227-test-usage
fix/rsvp-horizon-test-tuesday
fix/media-volume-owner
build/lane8-p3-editor
fix/912-unidentified-rename
build/lane8-p5-party-quests
build/lane8-handout-takeback
build/lane10-pickers
build/lane8-p2-article
build/lane8-6-map
build/lane10-7b-email-pages
build/lane8-5-handouts
build/lane10-7a-test-the-bot-pages
build/lane8-p1-pages
build/lane8-4-quests
build/lane8-2b-feeds
build/lane8-3-parties
build/lane8-2a-links
build/lane10-5-commands
build/lane9-6-seed
build/lane10-4b-reactions
build/lane9-4d-pages
build/lane9-4c-pages
build/lane10-4a-posts
build/lane10-3-email
fix/891-web-fonts
build/lane9-4b-pages
build/lane10-6-test-the-bot
build/lane10-2-notify-out
build/lane8-1-wiki-text
build/lane9-4a-frame
design/lane10-drawings
fix/j6-budget
fix/j5-phone-flake
fix/869-test-leaks
fix/843-record-start-followups
build/lane7-all
build/lane6-all
build/lane9-b2-server
chore/changelog-fragments
docs/decisions-d218
build/lane9-a-owner-overview
fix/rule-card-level-label
fix/834-retention-semantics
fix/o3c-record-without-announcement
docs/decisions-d179
docs/decisions-d131-d170
fix/851-commit-before-response
build/lane6-j3-seed
build/lane9-b1-d67-identity
fix/841-blocking-loop
build/lane9-c-backups-audit
fix/vote-race-and-start-commit
fix/recording-announcement
fix/proposal-read-leak
fix/last-admin-guard
build/lane6-p4c-planning
build/lane6-p4b-margin
build/lane6-p4a-sheet
build/lane5-j4-seed
build/lane6-base
build/lane7-pages-talk
build/lane5-pages
build/lane7-pages-safety
build/lane7-p3-safety
build/lane7-p2-notify
build/lane6-p3-planning
build/lane6-p2-history
build/lane6-p1b-items
build/lane7-p1-threads
build/lane5-p6-faces
build/lane6-p1a-sheet
build/lane5-p5-find
build/lane5-p4a-board
build/lane5-p4b-scenes
build/lane5-p3-live
build/lane5-base
build/lane5-p2-cards
docs/lane5-decisions
build/review-r2-wide
fix/review-r1
build/lane5-p1-scenes
build/demo-seed
build/lane4-journeys-j5
build/lane4-page-review-b
build/lane4-page-review-a
build/lane4-page-read
build/lane4-journeys
build/lane4-page-sess2
build/lane4-page-sess1
build/lane4-notes
build/lane4-discord-on-approval
build/lane4-scheduling
build/lane4-versions
build/760-session-number
build/lane4-decisions
build/797-erase-no-regenerate
build/test-race-audit
build/lane3-completions
build/794-dark-only
build/796-titles-scroll
build/791-retention-anchor
build/a11y-sweep
build/shell-followups
build/retention-preview
build/753-backup-readback
docs/d91-d93
build/lane3-j1
build/ci-harness-paths-selftest
fix/useauth-test-race
build/ci-harness-paths
build/777-ai-settings-read
build/lane3-settings-c
build/lane3-settings-b
build/lane3-j6
build/lane3-sessionform
build/lane3-settings-a
build/lane3-members
build/lane3-thisweek
build/lane3-story
chore/87-align-python-3.12
v4.8.0-rc1
v4.7.1
v4.7.1-rc1
v4.7.0
v4.7.0-rc4
v4.7.0-rc3
v4.7.0-rc2
v4.7.0-rc1
v4.6.2
v4.6.2-rc4
v4.6.2-rc3
v4.6.2-rc2
v4.6.2-rc1
v4.6.1
v4.6.1-rc1
v4.6.0
v4.6.0-rc2
v4.6.0-rc1
v4.5.3
v4.5.3-rc1
v4.5.2
v4.5.2-rc1
v4.5.1
v4.5.1-rc2
v4.5.1-rc1
v4.5.0
v4.5.0-rc12
v4.5.0-rc11
v4.5.0-rc10
v4.5.0-rc9
v4.5.0-rc8
v4.5.0-rc7
v4.5.0-rc6
v4.5.0-rc5
v4.5.0-rc4
v4.5.0-rc3
v4.5.0-rc2
v4.5.0-rc1
v4.4.1
v4.4.0
v4.3.0
v4.2.3
v4.2.2
v4.2.1
v4.2.0
v4.1.2
v4.1.1
v4.1.0
v4.0.1
v4.0.0
v3.11.4
v3.11.3
v3.11.2
v3.11.1
v3.11.0
v3.10.0
v3.9.1
v3.9.0
v3.8.0
v3.7.0
v3.6.1
v3.6.0
v3.5.0
v3.4.1
v3.4.0
v3.3.0
v3.2.0
v3.1.0
v3.0.0
v2.1.0
v2.0.0
v1.1.0
v1.0.0
v0.12.0
v0.11.0
v0.10.0
v0.9.1
v0.3.3
v0.9.0
v0.8.0
v0.3.2
v0.3.1
v0.3.0
v0.7.0
v0.6.0
v0.5.0
v0.4.0
v0.2.1
v0.2.0
v0.1.1
v0.1.0
Labels
Clear labels
backend
Issue related to the backend
bot
Issue related to the bot
bug
Something is not working
duplicate
This issue or pull request already exists
enhancement
New feature
frontend
Issue related to the frontend
help wanted
Need some help
invalid
Something is wrong
question
More information is needed
recording
Issue related to the recording system
security
Security hardening / vulnerability work
wontfix
This won't be fixed
No labels
backend
bot
bug
duplicate
enhancement
frontend
help wanted
invalid
question
recording
security
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Quest-Board#588
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Evidence, from the Gemma 4 26B run on the 2026-09-09 session (59 verified events):
The first pair is the same sentence at the same timestamp and should never have survived
dedupe_beats. The second is one moment reported twice a minute apart. There are also four separate events for waking up and realising the caverns moved them ("complete a long rest in the caverns", "complete a long rest", "wake up to find they have been moved", "realize they were in the shifting caverns").dedupe_beatscompares beats structurally (_same_eventon ranges and actors), so two extraction passes that phrase the same moment slightly differently, or stamp it a minute apart, both survive. That inflates the event log #568 introduced, wastes compose budget, and makes the adaptive pass loop think a pass found something new when it found the same thing again.Suggested approach. Extend
_same_eventwith a content comparison for beats whose ranges are close: normalised token overlap on the summary above a threshold, plus the same actor set, collapses to one — preferring the verified beat, then the one with more evidence citations, which is the preferencededupe_beatsalready encodes. Keep it conservative: two genuinely different events at the same second do happen (simultaneous speech is why ranged timestamps exist).Measure it on the replay, where the answer is known: 59 events with at least 6 collapsible, and the four waking-up rows should become one or two. The kind distribution is also worth recording while in here:
otherwas 27 of 59, which suggests the extraction prompt's type list does not fit exploration sessions well — possibly a separate issue.Related: #423 (why passes repeat), #568 (the log this inflates), #566 (ordering).
More evidence from run 3 (Gemma 4 26B on the dev replay, 2026-09-11 03:28 UTC, run
a0784433, at6533fa1with #591). 83 verified events, and the same-moment duplicates are still there:That's at least 10 surplus rows out of 83. Since #589 they matter for the prose too: the summary is written from 30 events stratified by time, so a triple costs the section it sits in two slots. The calliope triple and the detect magic triple sit in the 00:43 to 00:54 stretch, which is also where the summary lost the Thieves' Cant detail run 2 had kept.
The two identical-time cases are the cheapest win: same
t_startand the same normalised summary text is safe to collapse with no judgement at all.Picking up the judgement-free part only (2026-09-11), in one branch with #595.
beats_collapsedcount onsummarisation_runs. NULL means not measured, following the convention ofhallucination_dropsandname_snaps.The exact-duplicate rule is merged (PR #598, main
0935b7a) and deployed to dev. It was measured on run6d3e6f54(Gemma 4 26B, 2026-09-11 05:45 UTC).This run produced no exact duplicates, so the rule collapsed none. The log says: "Dedupe removed 126 beat(s) across the passes: 126 by shared citation and actor, 0 exact duplicate(s)". A normalised same-second, same-text query over the stored events agrees: 0 groups in run 4, against 3 in run 3. That's run-to-run variance in the model's output, not the rule at work. The rule is covered by 21 unit tests, but this run couldn't demonstrate it.
beats_collapsed= 126 is that total.What's left is rewordings, and there are more of them. Nine same-second pairs survived in run 4, each two phrasings of one moment:
Most of the pairs are one moment told from the GM's side and from the player's. That's the next thing to collapse: same second, overlapping citation window, one GM-only actor list and one player actor list. It needs a judgement rule, measured on this replay. It's also where #596's
table_adminlabel will change the picture, since several of the GM-side copies are description rather than events. So the judgement half here is best done after #596 is measured.Measured: a rule for "one moment, told from both sides" (read-only replay on dev)
The candidate rule
It would be a third fallback in
dedupe_beats, after_same_eventand_exact_duplicate.Results
Replayed on the three most recent unedited runs of session 5b0d6ac1:
Run 8's merges (kept event ← dropped event):
Checks in both directions:
Not covered
Near-identical GM rewordings a few seconds apart:
These could be caught by extending the exact rule to near-identical text within about 3 s, with the same actors.
Bridge descriptions worded differently (run 8). No lexical rule catches these.
The 01:08 journal rulings. They are a progression, and the last one carries Thieves' Cant, so they must not be merged.
Context
The owner's corrections (runs fdc09180 and 7dd357e7) include no struck duplicates, only rewrites and relabels. So this is a tidy-up rather than a fix the owner has asked for: fewer repeated rows, and about five compose slots freed per run.
The measurement says don't build it
Before writing the rule, I checked what it would actually change in the summary. It changes nothing good, and one variant of it does harm. Recommending this issue be closed with the exact-duplicate rule as its whole delivery.
The duplicates never cost a summary slot
For each same-moment pair in run 00e7962d, which half does the selector hand to compose?
Not one pair had both halves selected, and every pair that was represented was represented by the player's telling — which is exactly what the rule was going to enforce.
_spreadand the density-weighted fill already do this job. In run b0613da6 the same check found one pair where the GM's wording won, and the two texts were near-identical ("reveals the perception results for…" against "announces the results of the perception checks for…").Filtering before selection actively hurts
The obvious implementation — drop the GM-side copy before
select_beats_for_compose— is not neutral, because removing candidates changes the bucket densities that drive the fill. Replayed on run 00e7962d it kept the count at 36 and swapped four events, including:That is a strictly worse summary, produced by a rule whose merges were all individually correct. The lesson is that the selector is a tuned whole, and pruning its input is not a free improvement.
The GM repeating themselves is already gone
Same-actor repeats within 15 s, and whether both copies reached compose:
The one genuinely wasted slot is in
03b3acfa, which predates #603's event-picker fix. The three most recent runs have none at all.Recommendation
Close. The judgement-free half (
_exact_duplicate) shipped and earns its keep; the judgement half solves a problem the picker fixes in #596, #600 and #603 already removed. Worth revisiting only if a future session shows both halves of a pair selected — the replay above is the check to re-run.Closing on the owner's decision, per the measurement above.
Delivered: the judgement-free half —
_exact_duplicate, same start second and same normalised text, which removed real duplicates (a real run kept "Clio discovers the water is much deeper…" twice at 00:04:56) without any similarity judgement.Deliberately not delivered: the same-moment and split-moment halves. The measurement showed the event picker already hands compose the player's telling and never both halves of a pair, and that pruning the picker's input degrades the summary by shifting its bucket densities.
The check to re-run if this ever looks wrong again: for each candidate pair, ask which half
select_beats_for_composeactually returns. If a future session shows both halves selected, the premise has changed and this is worth reopening.