Five separate incidents, one architectural gap. OiMy had no authoritative idea of what "the current conversation" was — so every subsystem invented its own, and they all disagreed. Underneath sat a second fault: the model was never shown its own prior replies. This is what that gap was, how one bug was reproduced deterministically, why an earlier "confabulation" verdict was wrong, and what was built to close it.
This investigation covered bug #4 (a companion re-answering a snack question that had already been resolved three days earlier) and re-examined bug #5 (an "invented" detail), on top of the night's three earlier fixes: the bridge reach-back, the constraint-ledger thread race, and topic-cache staleness. All five are symptoms of the same gap.
OiMy has no first-class model of "the current conversation," and it never showed the model its own prior turns. Every subsystem that claimed "this session" or "current" semantics invented a private staleness window — and they disagreed. Meanwhile the model's outbound context carried only the user's side, so every question the user ever asked looked perpetually unanswered.
recent_messages has no session id. Seven mechanisms each defined "current" differently, from "last 24 rows of any age" to a "4-hour sliding envelope refreshed on every write."_call_companion stripped assistant rows from the outbound array — a deliberate 2026-07-04 decision — so it re-answered resolved questions and re-generated acknowledgments it had already given.created_at DESC with second-granularity timestamps and no id tiebreaker returned same-second Q/A pairs inverted.The intellectual center of this document. Neither face is a bug on its own — together they produce a companion that argues with a version of the conversation nobody actually had.
Because there is no session id on recent_messages, every subsystem that needed to reason about staleness rolled its own currency window. Here they are side by side — the same word, "current," meaning eight different things:
| Mechanism | Its private definition of "current" | Failure it caused |
|---|---|---|
| history fetch | last 24 rows, any age | 10 simulated days fed to the model as one live conversation |
h30 session_suggestions("Already said this session") |
4h sliding envelope, refreshed on every write | day-7 content still "this session" on day 10; the ban list + "advance the topic" steered the model onto the dead topic |
h30 session_ledger(constraints) |
same 4h sliding envelope | same latent landmine — "must honor" day-old constraints |
| [CONVERSATION DEPTH] turn counter |
len(24-row window) + 1 |
the first message of a new day arrived as "turn 9," firing "prioritize direct answers to the user's literal question" on turns that asked nothing |
| topic cache | turn % 3 + weak overlap check (fixed 07-10, 30-min TTL) |
day-old [CURRENT TOPIC] re-injected |
_kimi_jobs bridge |
none — spawn-time context (fixed 07-10) | N-turn-stale deliverables |
_current_user_id |
shared instance attr (fixed 07-10) | cross-user leakage — "current" not even scoped to the request |
active_threads |
300s TTL | worked — coincidentally |
A deliberate design decision. _call_companion dropped every assistant row before calling the model — a choice made in the 2026-07-04 recycle-check fix, which explicitly deferred restoring them as "a separate product decision." The result was that to the model, every question the user ever asked appears perpetually unanswered.
[Already said this session] block was band-aiding — with stale content — which is what turned a patch into a trap.
History queries ordered by created_at DESC with second-granularity timestamps and no id tiebreaker. Q/A pairs written in the same second came back inverted — verified live: rows 22916/22917 returned assistant-first — scrambling what little grounding the model still had.
The Whitaker loop, day 10 at 23:39 UTC. Every step below is verified against production data and logs. The request took route chat-turn → chat/unhandled fallback → _call_openrouter_companion on grok-4.3.
23:39 UTC
Model grok-4.3 · chat/unhandled fallback
[Already said this session — do NOT restate any of these, even reworded.
If the same topic returns... answer their literal question directly]
"All nut-free, nothing fancy, one store run." // day-7 answer tail
"Week of smooth mornings — that's the stretch
you're tracking." // day 9[CURRENT ASK] block was present to pull the model back to the here-and-now.The exact captured production prompt was replayed against grok-4.3 under two conditions. The only variable: whether assistant turns were interleaved into the history. The result is unusually clean — and the two arms landing on the same 3/3 count, with opposite meaning, is precisely the finding.
This one earns its own section because it is a genuine narrative turn: what looked like the model inventing a detail turned out to be real content it simply couldn't see it had already used. A softer-looking problem was, in fact, the same structural one.
The detail — "reading the reply once and moving on to Wren's lunch" — was confabulation: the model inventing a specific that the user never provided.
It was real content, verbatim from the user. Day 7 merely re-served an acknowledgment of it — because the model never saw its own day-6 reply that had already covered it.
Day 7's response re-served day 6's acknowledgment, near-duplicating day-6's own opener (row 22900) — which the model never saw, because assistant turns were stripped. Same root cause as bug #4, opposite surface: #4 re-answers a question, #5 re-acknowledges a fact.
It gets worse. When later challenged on the detail, OiMy falsely confessed to inventing it — because it had no way to check what either party had actually said:
The model apologized for a fabrication that never happened. It couldn't see the user's real words with correct attribution, and it couldn't see its own prior turns — so it had no ground truth to defend, and conceded to a wrong accusation. The confabulation diagnosis was itself a consequence of the same blindness.
Six changes, deployed together to prod and dev at ~00:39 UTC 2026-07-11. The through-line: compute "where the current session starts" once, then make every subsystem honor that one answer.
# module-level, matches the 30-min topic-cache TTL
_SESSION_GAP_S = 30 * 60
def _session_start_index(rows):
# scan back until a gap > _SESSION_GAP_S; that's the boundary
...
# computed ONCE at the top of _route_and_execute_inner, for EVERY path
_current_session_start = _session_start_index(recent_messages) # request-local
A module-level _session_start_index() (with _SESSION_GAP_S = 30 min, matching the topic-cache TTL), computed once at the top of _route_and_execute_inner for every path and stored in request-local _current_session_start.
session_suggestions and session_ledger items now carry timestamps. get_session_suggestions / get_constraint_ledger accept a since_ts and drop items from before the current session. Every injection site passes the request's session start.
The companion's outbound message array now includes assistant turns (_outbound_history in _call_companion). The repetition guards — [Already said], the variety governor, the recycle-check — remain in place as backstops, not primary defenses.
ORDER BY created_at DESC, id DESC on all four recent_messages history fetches. Same-second Q/A pairs no longer come back inverted.
In the chat_light path, so [CONVERSATION DEPTH] no longer fires on day-opening messages. The topic summarizer now gets session-scoped history, and a negative turn-gap marks the topic cache stale.
The [Already said this session] and [CONVERSATION DEPTH] copy now conditions "advance the topic / answer their literal question" explicitly on the user's current message raising it — closing the trap that pushed the model onto dead topics.
Backups were written before deploy in both trees: api.py.bak.pre-session-boundary-fix-2026-07-11 and archetypes/h30.py.bak.pre-session-boundary-fix-2026-07-11.
The fix ships with new tests, a passing existing suite, a dev end-to-end repro, and a production smoke test on the seeded scenario.
session_turn = 2, direct answer — confirming the boundary doesn't over-reset mid-conversation.These are genuine open items, presented as they stand — not sanded down. Some are follow-up work; some are deliberate decisions to leave a surface unchanged, flagged so the choice is visible rather than inherited.
Real fix: add a session_id column to recent_messages, assigned at write time using the same 30-min gap rule, so the boundary is computed once and stored — and becomes visible to analytics and evals — instead of recomputed on every request.
session_constraints ("[Do Not Suggest]") and session_openings (the variety governor) were left on the 4h sliding envelope on purpose. Cross-session memory is arguably desirable for both — don't re-suggest what failed last week; don't reuse yesterday's opener. This should be decided per-surface, deliberately, not by inheriting the default.
Giving the model its own phrasing lets it correctly avoid repetition — but if evals show verbatim self-quoting, the answer is to condense assistant turns older than the current session rather than drop them again. Watch the repetition metrics.
A user who pauses 35 minutes mid-conversation now resets the "session" — cost: mild repetition tolerance, and CONVERSATION DEPTH restarts. If evals show issues, the constant lives in exactly one place: _SESSION_GAP_S.
A "question-free share after a resolved ask, next day" probe is the sharpest detector of this entire bug class. It should be a permanent fixture in the eval harness, not a one-off from this investigation.
loop0_whitaker_4, day 10 at 23:39 UTC, route chat-turn → chat/unhandled → _call_openrouter_companion (grok-4.3).21:37, age 2h02m against the 4h sliding envelope.22916 / 22917 returned assistant-first, live.22899 (day 6) verbatim; re-served in day-7 near-duplicate of row 22900; false confession row 23070.grok-4.3, temp 0.7, 3 trials/arm — user-only 3/3 re-answered vs assistant-interleaved 3/3 grounded.api.py.bak.pre-session-boundary-fix-2026-07-11, archetypes/h30.py.bak.pre-session-boundary-fix-2026-07-11.