Skip to content

Commit 146dc66

Browse files
committed
fix(timeline): nominate an episode image whenever cited evidence has one
The Rematch gaming session came back with no picture even though it cited an observation carrying a frame: `representative_evidence_id` was described as "optional", to be left null "when no suitable image is available", and the agent read a gaming session as not needing one. Four of thirteen episodes got an image on a day where ninety-one evidence items advertised one. It is now stated as what it is — how an episode gets its picture on the timeline — with null reserved for an episode citing no image-bearing evidence at all. PROMPT_VERSION and the shipped pin move to v10 so existing days re-run.
1 parent eca55c9 commit 146dc66

3 files changed

Lines changed: 92 additions & 9 deletions

File tree

backends/advanced/src/advanced_omi_backend/services/timeline/prompt.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44

5-
PROMPT_VERSION = "timeline-episodes-v6"
5+
PROMPT_VERSION = "timeline-episodes-v10"
66

77

88
OUTPUT_SCHEMA = {
@@ -143,6 +143,22 @@ def build_prompt(output_path: str) -> str:
143143
because they are adjacent. Use separate episodes, or a parent session with supported
144144
child events, when the evidence distinguishes them.
145145
- Episodes may overlap; long passive media and simultaneous foreground work can coexist.
146+
- One activity spans every modality that evidences it. When screen and audio evidence
147+
cover the same stretch of the same activity, cite both from one episode. Do not emit an
148+
audio episode running alongside a screen episode for the same minutes — that is a
149+
modality split, not two events.
150+
- Only create a standalone audio episode when no concurrent foreground activity explains
151+
the sound. "Background audio continued" beside a game, call, or meeting episode covering
152+
those same minutes is the split this rule forbids.
153+
- Role is a property of each evidence item, not of the episode. One episode may cite
154+
`media_content` output audio, `uncertain` microphone input, and `application_state`
155+
screen evidence together; the people and application named by any of them belong in
156+
that episode's entities.
157+
- When cited evidence names a person, use that name in the title, summary, and entities.
158+
Never replace a name that appears in the evidence with a placeholder such as "a
159+
friend", "someone", or "another person". Use a generic term only when no name was
160+
captured. Uncertainty about a claim belongs in `confidence` and the assertion's role,
161+
not in blurring who was there.
146162
- Use quiet, idle, ambient, or unknown episodes when evidence genuinely supports them.
147163
- `output` audio/transcripts are media or system content, never the user's statements.
148164
- `input` audio is uncertain unless speaker evidence supports user attribution.
@@ -158,11 +174,17 @@ def build_prompt(output_path: str) -> str:
158174
- Account for every evidence-bearing interval with one or more episodes or an explicit
159175
`unassigned_interval`. Unassigned intervals must be positive and inside this day.
160176
- Never return both `episodes` and `unassigned_intervals` empty when evidence exists.
161-
- `representative_evidence_id` is optional and, when set, must name image evidence
162-
already cited by that episode. Use null when no suitable image is available.
177+
- `representative_evidence_id` is how an episode gets its picture on the timeline. Set
178+
it whenever the episode cites any evidence carrying an `image_filename`, choosing the
179+
frame that best depicts what the episode was; only use null when none of the evidence
180+
it cites has one. It must name evidence that episode already cites.
163181
- Never invent an evidence ID. Chronicle tracks authoritative window coverage itself;
164182
do not echo window IDs into the result.
165183
- Salience is display value, not confidence.
166184
- Express optional episode metadata as short string key/value entries in `attributes`.
167185
- Prefer a few coherent episodes over arbitrary periodic fragments.
186+
- Confirmed episodes, when supplied, are settled by the person whose day this is. Treat
187+
their intervals as already accounted for: do not re-segment them, do not emit an
188+
episode overlapping them, and do not list their time as unassigned. You may still cite
189+
evidence inside a confirmed interval from an episode that mostly lies outside it.
168190
"""

backends/advanced/tests/test_timeline_prompt.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from pathlib import Path
2+
3+
import yaml
4+
15
from advanced_omi_backend.services.timeline.prompt import (
26
OUTPUT_SCHEMA,
37
PROMPT_VERSION,
@@ -27,10 +31,53 @@ def test_timeline_output_schema_requires_grounded_episodes_and_assertions():
2731
assert assertion["properties"]["evidence_ids"]["minItems"] == 1
2832

2933

34+
def test_prompt_version_is_pinned():
35+
"""Changing the prompt text without bumping this leaves cached runs on the old rules.
36+
37+
Asserted once, here — not inside each content test, where two of them disagreeing is
38+
the only thing a second copy can achieve.
39+
"""
40+
41+
assert PROMPT_VERSION == "timeline-episodes-v10"
42+
43+
3044
def test_prompt_treats_observations_as_coarse_sessions_without_cross_app_merging():
3145
prompt = build_prompt("result.json")
3246

33-
assert PROMPT_VERSION == "timeline-episodes-v6"
3447
assert "coarse application session" in prompt
3548
assert "Do not merge distinct foreground applications" in prompt
3649
assert "manufacture periodic sub-events" in prompt
50+
51+
52+
def test_prompt_tells_the_agent_to_leave_confirmed_intervals_alone():
53+
prompt = build_prompt("result.json")
54+
55+
assert "Confirmed episodes" in prompt
56+
assert "do not re-segment them" in prompt
57+
58+
59+
def test_prompt_keeps_one_activity_in_one_episode_across_modalities():
60+
"""A game with a friend must not split into a screen episode and an audio episode."""
61+
62+
prompt = build_prompt("result.json")
63+
64+
assert "One activity spans every modality that evidences it" in prompt
65+
assert "modality split, not two events" in prompt
66+
assert (
67+
"Only create a standalone audio episode when no concurrent foreground" in prompt
68+
)
69+
70+
71+
def test_shipped_config_pins_the_same_prompt_version():
72+
"""Run identity comes from config, not this constant — they must not drift.
73+
74+
`request_timeline_analysis` keys a run on `timeline.prompt_version` from
75+
`config/defaults.yml`. A prompt edit that bumps only the constant changes nothing:
76+
completed days stay cached on the old rules and no reanalysis is triggered.
77+
"""
78+
79+
defaults = yaml.safe_load(
80+
(Path(__file__).resolve().parents[3] / "config" / "defaults.yml").read_text()
81+
)
82+
83+
assert defaults["timeline"]["prompt_version"] == PROMPT_VERSION

config/defaults.yml

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,10 @@ cron_jobs:
926926
timeline_analysis:
927927
description: "Reconcile changed current-day capture evidence into semantic episodes"
928928
enabled: true
929-
schedule: "*/30 * * * *"
929+
# Every run regenerates the whole day from scratch — there is no incremental merge —
930+
# so a tight cadence re-does the same work and reshuffles episode ids each time.
931+
# Analysis is also available on demand from the Timeline page.
932+
schedule: "0 */4 * * *"
930933

931934
observation_curation:
932935
codex:
@@ -935,15 +938,26 @@ observation_curation:
935938
timeout_seconds: 600
936939

937940
timeline:
938-
window_minutes: 20
941+
# Window size is a capacity knob, not a semantic one — the prompt treats windows as
942+
# coverage units that must never become episode boundaries. It sets how many files
943+
# one agent pass must read: 20min/3overlap put a full day at 85 windows, which never
944+
# once produced episodes, while days under ~18 windows nearly always succeeded.
945+
# 60min puts a full day at 26. The per-window text cap is raised to match so fewer,
946+
# larger windows do not truncate more evidence than the old layout carried.
947+
window_minutes: 60
939948
overlap_minutes: 3
940-
max_text_chars_per_window: 30000
949+
max_text_chars_per_window: 60000
941950
max_anchor_images_per_window: 4
942951
executor: codex
943-
prompt_version: timeline-episodes-v6
952+
# Run identity. Must match services/timeline/prompt.py's PROMPT_VERSION; changing the
953+
# prompt text without bumping both leaves completed days cached on the old rules.
954+
prompt_version: timeline-episodes-v10
944955
codex:
945956
model: gpt-5.6-luna
946-
reasoning_effort: low
957+
# `low` was chosen for cost, but a pass that returns no episodes costs a full run
958+
# (~600k input tokens) and delivers nothing — the cheap setting was the expensive
959+
# one. An empty result now escalates one step and retries.
960+
reasoning_effort: medium
947961
# Reuse the memory agent's container-aware sandbox choice. Rootless Podman
948962
# deployments set that to danger-full-access because the container itself is
949963
# the isolation boundary and nested bubblewrap cannot start there.

0 commit comments

Comments
 (0)