Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.es-ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ legible. `grep` escanea bajo demanda **todas** las transcripciones indexadas —
turnos internos (salida de herramientas, razonamiento) que nunca se convirtieron en
fragmentos de búsqueda. Solo bajo demanda: sin inyección proactiva de contexto en cada prompt.

**A qué proyecto pertenece una sesión.** El `project` de un ancla es la carpeta donde se
lanzó la sesión; sus `repos` son aquello en lo que la sesión trabajó de verdad. Cada ruta que
nombran sus llamadas a herramientas (una edición, un `cd`, un `workdir` de Codex) se resuelve
al repo git que la contiene — los worktrees y los alias por symlink colapsan a un solo
checkout — y un repo distinto de la carpeta de lanzamiento necesita al menos tres llamadas
para contar. Pasa `project="harmonia"` a cualquier herramienta de descubrimiento para obtener
las sesiones que trabajaron en ese repo, se lanzaran donde se lanzaran; `scope_cwd` también
las incluye. `recall_search` devuelve como mucho dos anclas por sesión (`per_session`, `0` =
sin límite) para que una sesión larga no llene la página, y recorta cada fragmento alrededor
del pasaje que coincide con la consulta. `recent_sessions` se lee como un tablero de estado:
la primera petición real, la última petición y respuesta, rama, repos, `started_by` (`user`,
`agent` o `automation`, inferido de los metadatos de la transcripción) y `active` (un turno
en los últimos 10 minutos).

<details>
<summary><strong>Ver una llamada completa</strong></summary>

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ Ranked anchors carry provenance and a human-readable timestamp. `grep` scans **a
transcripts on demand — including under-the-hood turns (tool output, thinking) that never
became search chunks. On-demand only: no proactive context injection into every prompt.

**Which project a session belongs to.** A hit's `project` is the folder the session was
launched in; its `repos` are what that session actually worked on. Every path its tool calls
name (an edit, a `cd`, a Codex `workdir`) is resolved to the owning git repo — worktrees and
symlinked aliases collapse to one checkout — and a repo other than the launch folder needs at
least three calls to count. Pass `project="harmonia"` to any discovery tool to get the
sessions that worked on that repo wherever they ran; `scope_cwd` includes them too.
`recall_search` keeps at most two hits per session (`per_session`, `0` = off) so one long
session cannot fill the page, and cuts each snippet around the passage that matches the
query. `recent_sessions` reads like a status board: the first real ask, the latest ask and
answer, branch, repos, `started_by` (`user`, `agent` or `automation`, inferred from
transcript metadata) and `active` (a turn in the last 10 minutes).

<details>
<summary><strong>See a complete call</strong></summary>

Expand Down
10 changes: 10 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ flowchart LR
`grep` 按需扫描**所有**已索引的会话记录——包括那些从未成为搜索分块的幕后轮次
(工具输出、思考过程)。一切只按需进行:不会向每条提示词主动注入上下文。

**会话属于哪个项目。** 锚点上的 `project` 是会话启动时所在的文件夹;`repos` 才是该会话真正
动过的仓库。工具调用里出现的每个路径(一次编辑、一次 `cd`、Codex 的 `workdir`)都会解析到
拥有它的 git 仓库——worktree 和符号链接别名会折叠成同一个检出——而启动文件夹以外的仓库至少
要被调用三次才算数。给任何检索工具传 `project="harmonia"`,就能拿到所有动过这个仓库的会话,
无论它们在哪里启动;`scope_cwd` 也会包含它们。`recall_search` 每个会话最多保留两条结果
(`per_session`,`0` 表示不限),避免一个长会话占满整页,并把摘要截在与查询匹配的段落附近。
`recent_sessions` 读起来像一块状态看板:第一个真正的请求、最新的请求和回答、分支、仓库、
`started_by`(`user`、`agent` 或 `automation`,依据会话记录的元数据推断)以及 `active`
(最近 10 分钟内有过轮次)。

<details>
<summary><strong>查看一次完整调用</strong></summary>

Expand Down
13 changes: 13 additions & 0 deletions docs/README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ flowchart LR
подкапотные реплики (вывод инструментов, размышления), которые так и не стали поисковыми
чанками. Только по требованию: никакой проактивной инъекции контекста в каждый промпт.

**К какому проекту относится сессия.** `project` у хита — папка, где сессию запустили;
`repos` — репозитории, с которыми она на самом деле работала. Каждый путь из вызовов
инструментов (правка файла, `cd`, `workdir` у Codex) сводится к git-репозиторию-владельцу —
worktree и симлинки-псевдонимы схлопываются в один чекаут, — а чужому репо, кроме папки
запуска, нужно минимум три вызова, чтобы засчитаться. Передайте `project="harmonia"` в любой
поисковый инструмент — получите сессии, которые работали с этим репо, где бы их ни запустили;
`scope_cwd` их тоже включает. `recall_search` берёт не больше двух хитов на сессию
(`per_session`, `0` — без лимита), чтобы одна длинная сессия не забивала выдачу, и режет
сниппет вокруг места, совпавшего с запросом. `recent_sessions` читается как доска статусов:
первый настоящий запрос, последний запрос и ответ, ветка, репозитории, `started_by` (`user`,
`agent` или `automation`, по метаданным транскрипта) и `active` (реплика за последние
10 минут).

<details>
<summary><strong>Посмотреть полный вызов</strong></summary>

Expand Down
107 changes: 107 additions & 0 deletions docs/decisions/2026-10-25-recall-for-registrar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Recall as the registrar's memory: repo attribution, status board, diverse hits

## Контекст

Максим проектирует «Командный центр»: он говорит только с регистратором — LLM-
коннектором, у которого из инструментов один поиск. Регистратор должен отвечать
«что происходит в проекте X», «кто из агентов что делает», «к какому проекту это
относится», «что мы решили по Y», и его память — session-recall. Аудит живого
индекса 25.10 (875 сессий, Qwen3-Embedding-8B) показал, что на эти вопросы recall
отвечает плохо:

- `project` — это папка запуска. Сессия 82ef7c9a числилась `food_sferyx` (запущена
в его worktree), а сделала 254 файловые операции в `~/harmonia` и ни одной в
food_sferyx. `recall_search(scope_cwd=~/harmonia)` вернул 2 нерелевантных хита.
`~/exodia` — симлинк на `~/whytap`, но в индексе это два проекта.
- `recent_sessions.label` — первый промпт, а он часто служебный
(`<side-sessions-event>` 95 раз, бриф side-сессии 40, `<pasted_content>` 24). Нет
последнего запроса/ответа, ветки, того, кто открыл сессию.
- «что происходит в проекте Гармония» → 7 из 10 хитов из одной сессии.
- Сниппет — первые 200 символов; у 681 сжатого пересказа это всегда одна и та же
шапка «This session is being continued…».

## Решение

1. **Атрибуция к репо по вызовам инструментов** (`repos.py`). Каждый путь во
ВХОДАХ вызовов (Edit `file_path`, `cd`, `git -C`, Codex `workdir`, код Codex
`exec`) сводится к git-репо: подъём по ФС до `.git`, `.git`-файл worktree ведёт к
основному чекауту, затем `realpath` (симлинк-псевдоним → один репо). Вызов без
абсолютного пути засчитывается папке запуска. Чужому репо нужно ≥3 вызова; папка
запуска засчитывается всегда (если не git — под своим именем). Факты лежат в
`session_repos` / `session_info` / `repo_scans`, по `file_path`, в той же
пофайловой транзакции, что и чанки.
2. **`project=` во всех поисковых тулзах** и расширенный `scope_cwd`: сессия
проходит фильтр, если запущена в репо ИЛИ работала с ним. `scope_cwd` из подпапки
сводится к корню репо. Anchor получил поле `repos`.
3. **`recent_sessions` как доска статусов**: `label` = первый настоящий запрос
(из брифа side-сессии — блок `Task:`), `last_user`, `last_assistant`,
`git_branch`, `repos`, `started_by` (`user` / `agent` / `automation`), `active`.
4. **Не больше 2 хитов на сессию** в `recall_search` (`per_session`, 0 = выкл.), с
добором в порядке ранга, если сессий меньше, чем слотов. Реранкер теперь
упорядочивает все кандидаты, а не top-k.
5. **Сниппет по запросу** (`snippets.py`): окно, покрывающее больше всего слов
запроса (префиксное совпадение — «Гармония»/«Гармонии»), шапка compaction-
пересказа срезается; без буквального совпадения — начало текста.

## Почему

- Вызовы инструментов — единственное надёжное свидетельство, с каким кодом
работала сессия; текст ответа упоминает чужие репо постоянно, а результаты
инструментов (листинги, `ls ~`) — тем более, поэтому учитываются только входы.
- Скан — только текст, без эмбеддингов: старый индекс догоняется одним потоковым
проходом, без повторной оплаты эмбеддера и без смены `EXTRACTOR_VERSION`.
- `started_by` берётся из метаданных (`entrypoint` у Claude, `originator`/`source`
у Codex), а не из слов: промпт подделать легко, метаданные — нет. Две текстовые
формы (бриф side-сессии, `<heartbeat>`) — то, что оставляет сама программа.
- Лимит на сессию с добором: регистратору нужен разрез по сессиям, но узкая
история не должна возвращать меньше k.

## Что протестировали

- `tests/test_registrar.py` — сценарии из живого индекса: сессия в worktree
одного репо правит другой; симлинк-псевдоним; «взгляд» на соседний README и
чтение плагин-кэша (git-клон в `~/.claude`) не дают атрибуции; удалённый
worktree и не-git папка; scratch-папка в `~/Library`; Codex `workdir` + код
`exec` + `originator: Claude Code`; догон старого индекса без единого нового
эмбеддинга; рост и удаление транскрипта; доска статусов с брифом,
`<side-sessions-event>`, `<pasted_content>`, heartbeat, `sdk-cli`; лимит на
сессию и добор; сниппет у compaction-пересказа; проброс `project` через MCP и
клиент хаба.
- Первая версия `grep` с фильтром `project` молча теряла все хиты: локальная
переменная цикла перезаписывала параметр `project` — поймал существующий тест
Codex-интеграции.
- `scope_cwd` из подпапки (`~/harmonia/src`) сначала не находил работу из других
папок: сравнивался путь, а не корень репо. Исправлено отдельным подъёмом до
`.git` без фильтров атрибуции.
- Первый прогон на клоне живого индекса завис на первом же транскрипте: хвост
URL `https://github.com/…` распознавался как путь `//github.com/…`, а POSIX
`dirname("//") == "//"` — подъём к корню не кончался. Плюс `repo_of("/")` через
`normpath("")` превращался в `.` и находил репо текущего каталога. Оба случая
закрыты и покрыты тестом; после фикса тот же транскрипт сканируется за 0.01 с.
- Второй прогон показал дыру: Codex держит worktree в `~/.codex/worktrees/<id>/<repo>`,
а скрытые папки отбрасывались до поиска `.git` — 44 сессии whytap остались без
атрибуции. Теперь скрытая папка судится по репо, к которому ведёт (worktree →
основной чекаут засчитывается, git-клон плагина в `~/.claude` — нет), а удалённый
Codex-worktree сводится к одноимённому чекауту в `~`. После этого 0 сессий
whytap/exodia без атрибуции.
- Клон живого индекса (876 сессий, 620 транскриптов, 6.8 GB): догон фактов —
25 с, без единого нового эмбеддинга. whytap: 224 сессии (было 89 + 136 под
двумя именами); harmonia: 5 сессий вместо 1; 4 сессии «food_sferyx» на деле
работали в другом репо; `started_by`: 451 user / 177 agent.
- Живые запросы: «что происходит в проекте Гармония» — 6 разных сессий в топ-10
вместо 4 (7 хитов было из одной); `scope_cwd=~/harmonia` находит работу из
worktree food_sferyx вместо двух нерелевантных хитов. Локальная часть поиска
(knn+fts) — 0.8 с против 1.0 с до изменений; реранк 100 кандидатов не дороже 10.

## Отвергли

- Определять проект по тексту (упоминания имени репо) — шум: MEMORY.md с именами
всех проектов вставлен в каждую сессию.
- `git remote` вместо пути — нужен `git` на каждый путь и не работает на хабе.
- Переписать `chunks.project` — пришлось бы перевыпускать весь индекс и ломать
смысл поля «где запущено», на который опираются старые клиенты.
- Отдельная тулза `project_state` — следующий шаг поверх атрибуции (пункт 6
аудита), не в этом PR.
- Чистка шума в `grep` (пункт 5 аудита) — отдельным заходом.

2026-10-25 · PR: см. историю ветки `claude/recall-for-registrar-1220aa`
8 changes: 5 additions & 3 deletions src/session_recall/hub/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ def _search(self, owner: str, body: dict) -> None:
hits = hub.recall.recall_search(
query, k=int(body.get("k") or 10),
scope_cwd=body.get("scope_cwd"), source=body.get("source"),
start_ts=start_ts, end_ts=end_ts)
start_ts=start_ts, end_ts=end_ts, project=body.get("project"),
per_session=int(body.get("per_session", 2)))
owners = owners_for(hub, (a.session_id for a in hits))
self._send(200, {
"anchors": [_anchor(a, owners.get(a.session_id)) for a in hits],
Expand Down Expand Up @@ -302,7 +303,7 @@ def _grep(self, owner: str, body: dict) -> None:
hits = hub.recall.grep(
pattern, body.get("session_id"), scope_cwd=body.get("scope_cwd"),
source=body.get("source"), limit=int(body.get("limit") or 100),
start_ts=start_ts, end_ts=end_ts)
start_ts=start_ts, end_ts=end_ts, project=body.get("project"))
owners = owners_for(hub, (a.session_id for a in hits))
self._send(200, {
"anchors": [_anchor(a, owners.get(a.session_id)) for a in hits]})
Expand All @@ -311,7 +312,8 @@ def _recent(self, owner: str, body: dict) -> None:
start_ts, end_ts = _range(body)
sessions = hub.recall.recent_sessions(
scope_cwd=body.get("scope_cwd"), limit=int(body.get("limit") or 10),
source=body.get("source"), start_ts=start_ts, end_ts=end_ts)
source=body.get("source"), start_ts=start_ts, end_ts=end_ts,
project=body.get("project"))
owners = owners_for(hub, (s["session_id"] for s in sessions))
for session in sessions:
session["owner"] = owners.get(session["session_id"])
Expand Down
Loading
Loading