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
Collaborator

Evidence, from the Gemma 4 26B run on the 2026-09-09 session (59 verified events):

[04:14] other   Idani investigates the depth of the water.
[04:14] other   Idani investigates the depth of the water.
[69:54] reveal  The GM reveals that Wyatt can interpret the map to find directions and a nearby city.
[71:26] reveal  The GM reveals that Wyatt can see a mid-sized city and a path to it on the map.

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_beats compares beats structurally (_same_event on 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_event with 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 preference dedupe_beats already 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: other was 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).

**Evidence, from the Gemma 4 26B run on the 2026-09-09 session** (59 verified events): ``` [04:14] other Idani investigates the depth of the water. [04:14] other Idani investigates the depth of the water. [69:54] reveal The GM reveals that Wyatt can interpret the map to find directions and a nearby city. [71:26] reveal The GM reveals that Wyatt can see a mid-sized city and a path to it on the map. ``` 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_beats` compares beats structurally (`_same_event` on 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_event` with 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 preference `dedupe_beats` already 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: `other` was 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).
Author
Collaborator

More evidence from run 3 (Gemma 4 26B on the dev replay, 2026-09-11 03:28 UTC, run a0784433, at 6533fa1 with #591). 83 verified events, and the same-moment duplicates are still there:

  • Identical time:
    • 00:04:56 "Clio discovers the water is much deeper…", twice, word for word
    • 00:34:29 "The GM confirms there are plants growing in the area", twice
    • 00:47:09 "Viquilonto (Viq) finds a journal tucked under a rock", twice
  • Same time, different wording:
    • 00:09:08, 2 variants of "Clio catches Idani with the rope"
    • 00:15:04, 2 variants of "finds a rotting skeleton"
  • Split over seconds:
    • 00:53:26 / 00:53:33 / 00:53:36, the calliope emblem, three times
    • 00:43:34 / 00:44:08 / 00:44:34, the detect magic results, three times
    • 00:54:16 / 00:55:05, "The GM explains what a thresher is", twice

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_start and the same normalised summary text is safe to collapse with no judgement at all.

**More evidence from run 3** (Gemma 4 26B on the dev replay, 2026-09-11 03:28 UTC, run `a0784433`, at 6533fa1 with #591). 83 verified events, and the same-moment duplicates are still there: - **Identical time:** - 00:04:56 "Clio discovers the water is much deeper…", twice, word for word - 00:34:29 "The GM confirms there are plants growing in the area", twice - 00:47:09 "Viquilonto (Viq) finds a journal tucked under a rock", twice - **Same time, different wording:** - 00:09:08, 2 variants of "Clio catches Idani with the rope" - 00:15:04, 2 variants of "finds a rotting skeleton" - **Split over seconds:** - 00:53:26 / 00:53:33 / 00:53:36, the calliope emblem, three times - 00:43:34 / 00:44:08 / 00:44:34, the detect magic results, three times - 00:54:16 / 00:55:05, "The GM explains what a thresher is", twice 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_start` and the same normalised summary text is safe to collapse with no judgement at all.
Author
Collaborator

Picking up the judgement-free part only (2026-09-11), in one branch with #595.

  • The change: collapse beats with the same start timestamp and the same normalised text, keeping the first. The rows listed in the previous comment at 00:04:56, 00:34:29 and 00:47:09 are the target.
  • Recorded: a nullable beats_collapsed count on summarisation_runs. NULL means not measured, following the convention of hallucination_drops and name_snaps.
  • Leaving this issue open for same-moment rewordings and split moments, e.g. the calliope emblem at 00:53:26, 00:53:33 and 00:53:36. Those need judgement, and deserve their own measured change.
**Picking up the judgement-free part only** (2026-09-11), in one branch with #595. - **The change:** collapse beats with the same start timestamp and the same normalised text, keeping the first. The rows listed in the previous comment at 00:04:56, 00:34:29 and 00:47:09 are the target. - **Recorded:** a nullable `beats_collapsed` count on `summarisation_runs`. NULL means not measured, following the convention of `hallucination_drops` and `name_snaps`. - **Leaving this issue open** for same-moment rewordings and split moments, e.g. the calliope emblem at 00:53:26, 00:53:33 and 00:53:36. Those need judgement, and deserve their own measured change.
Author
Collaborator

The exact-duplicate rule is merged (PR #598, main 0935b7a) and deployed to dev. It was measured on run 6d3e6f54 (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:

  • 00:04:56 Clio on the water: "deeper, colder, and faster-moving" / "deeper, very cold, and moving deceptively"
  • 00:14:42 "discovers a campfire" / "finds a campfire in the cave"
  • 00:15:04 the skeleton, once as Harrowhark and Viq, once as Harrowhark
  • 00:39:55, 00:40:11, 00:40:24: GM descriptions paired with the players' readings of the same moment
  • 00:47:09 finding the journal, 00:47:36 its contents, 00:53:33 the calliope emblem, each told once from the GM's side and once from the player's

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_admin label 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.

**The exact-duplicate rule is merged** (PR #598, main `0935b7a`) and deployed to dev. It was measured on run `6d3e6f54` (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: - 00:04:56 Clio on the water: "deeper, colder, and faster-moving" / "deeper, very cold, and moving deceptively" - 00:14:42 "discovers a campfire" / "finds a campfire in the cave" - 00:15:04 the skeleton, once as Harrowhark and Viq, once as Harrowhark - 00:39:55, 00:40:11, 00:40:24: GM descriptions paired with the players' readings of the same moment - 00:47:09 finding the journal, 00:47:36 its contents, 00:53:33 the calliope emblem, each told once from the GM's side and once from the player's 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_admin` label 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.
Author
Collaborator

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_event and _exact_duplicate.

  • Which pairs are considered: two verified story events within 10 s of each other, where one is GM-only and the other has a player actor.
  • When they match (content words exclude stopwords, narration verbs and character names). Any one of these is enough:
    • they share a citation, and the GM event either names the player or shares a content word with the player's event
    • the GM event names the player and they share a content word
    • they share two or more content words
  • Which survives: the player-side event, with its own evidence. No fields are merged, as with the other two rules.

Results

Replayed on the three most recent unedited runs of session 5b0d6ac1:

run verified story events merges false merges
03b3acfa 43 (30 GM-only) 0 0
b0613da6 54 2 0
00e7962d (run 8) 65 5 0

Run 8's merges (kept event ← dropped event):

  • Idani inspects the depth of the water ← the GM narrates Idani looking at the water
  • Harrowhark can't place herself in the brightness ← the GM describes the light
  • Viq senses sound from the south-east ← the GM says sounds come from the south-east
  • Clio casts detect magic ← the GM describes the results
  • Viq finds the journal ← the GM describes the journal Viq finds

Checks in both directions:

  • Correctly kept apart: "the GM narrates Idani checking the water" and Clio's nature check (4 s apart, one shared word).
  • Missed, and kept by design: in b0613da6, "the GM describes the light" and "the GM describes the visual conditions for Harrowhark". They share only a citation.
  • No false merges.

Not covered

  • Near-identical GM rewordings a few seconds apart:

    • "describes the appearance and condition of the bridge" at 00:26:46 and 00:26:48
    • "reveals the party's current location is Ironroot" at 00:49:28 and 00:49:31

    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.

## 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_event` and `_exact_duplicate`. - **Which pairs are considered:** two verified story events within 10 s of each other, where one is GM-only and the other has a player actor. - **When they match** (content words exclude stopwords, narration verbs and character names). Any one of these is enough: - they share a citation, and the GM event either names the player or shares a content word with the player's event - the GM event names the player and they share a content word - they share two or more content words - **Which survives:** the player-side event, with its own evidence. No fields are merged, as with the other two rules. ### Results Replayed on the three most recent unedited runs of session 5b0d6ac1: | run | verified story events | merges | false merges | |---|---|---|---| | 03b3acfa | 43 (30 GM-only) | 0 | 0 | | b0613da6 | 54 | 2 | 0 | | 00e7962d (run 8) | 65 | 5 | 0 | **Run 8's merges** (kept event ← dropped event): - Idani inspects the depth of the water ← the GM narrates Idani looking at the water - Harrowhark can't place herself in the brightness ← the GM describes the light - Viq senses sound from the south-east ← the GM says sounds come from the south-east - Clio casts detect magic ← the GM describes the results - Viq finds the journal ← the GM describes the journal Viq finds **Checks in both directions:** - **Correctly kept apart:** "the GM narrates Idani checking the water" and Clio's nature check (4 s apart, one shared word). - **Missed, and kept by design:** in b0613da6, "the GM describes the light" and "the GM describes the visual conditions for Harrowhark". They share only a citation. - **No false merges.** ### Not covered - **Near-identical GM rewordings a few seconds apart:** - "describes the appearance and condition of the bridge" at 00:26:46 and 00:26:48 - "reveals the party's current location is Ironroot" at 00:49:28 and 00:49:31 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.
Author
Collaborator

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?

pair outcome
Idani inspects the depth / GM narrates Idani looking player only
Harrowhark in the brightness / GM describes the light neither selected
Viq senses sound SE / GM says sounds SE player only
Clio casts detect magic / GM describes the results player only
Viq finds the journal / GM describes the journal Viq finds player only

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. _spread and 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:

  • lost: "Viquilonto (Viq) finds a journal tucked under a rock" — the journal is the session's central discovery
  • gained: "The GM describes the appearance of the bridge" — a third description of the same bridge

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:

run repeats both selected
7dd357e7 (corrected) 0 —
fdc09180 (corrected) 0 —
00e7962d (run 8) 0 —
b0613da6 1 no
03b3acfa 2 1
a0784433 2 no

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.

## 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? | pair | outcome | |---|---| | Idani inspects the depth / GM narrates Idani looking | **player only** | | Harrowhark in the brightness / GM describes the light | neither selected | | Viq senses sound SE / GM says sounds SE | **player only** | | Clio casts detect magic / GM describes the results | **player only** | | Viq finds the journal / GM describes the journal Viq finds | **player only** | 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. `_spread` and 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: - **lost:** "Viquilonto (Viq) finds a journal tucked under a rock" — the journal is the session's central discovery - **gained:** "The GM describes the appearance of the bridge" — a third description of the same bridge 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: | run | repeats | both selected | |---|---|---| | 7dd357e7 (corrected) | 0 | — | | fdc09180 (corrected) | 0 | — | | 00e7962d (run 8) | 0 | — | | b0613da6 | 1 | no | | 03b3acfa | 2 | **1** | | a0784433 | 2 | no | 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.
Author
Collaborator

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_compose actually returns. If a future session shows both halves selected, the premise has changed and this is worth reopening.

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_compose` actually returns. If a future session shows **both** halves selected, the premise has changed and this is worth reopening.
Sign in to join this conversation.
No milestone
No project
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
No description provided.