Как устроены задачи
Техническая сторона задач: одна таблица, ключи, отношения, бронь и планируемая проверка на дубликаты.
Разберитесь, как хранится задача, откуда берётся её ключ и как инструмент будет находить задачи, которые могут быть одной. Это пригодится разработчикам и авторам агентов, которые читают локальное хранилище или строят что-то поверх задач. Сначала — данные, потом планируемое: ключи по типам, бронь и проверка на дубликаты с разобранным примером. Для повседневной работы начните со страницы Задачи.
Устройство описано в утверждённом плане задач в cli-core (WireCatLabs/cli-core#1216). В каждом разделе сказано, что уже есть, а что планируется.
Данные
Сейчас. Одна таблица, tasks, хранит все задачи локального хранилища. Полный список столбцов —
в модели данных.
| Что | Как хранится |
|---|---|
| Тип | question, request, mention, promise для задач из сообщений; bug, feature, chore для задач проекта |
| План | название, описание, приоритет от 0 (срочно) до 4 (низкий), срок, время следующей проверки, родительская задача |
| Состояние | open, done или dismissed, с тем, кто закрыл и почему |
| Источник | один указатель на сообщение или заметку, никогда не текст сообщения, поэтому удалённое сообщение ничего не оставляет |
| Оценка | useful или not_useful — как вы оценили задачу, поднятую агентом |
| Люди | назначения с ролью: исполнитель, проверяющий, наблюдатель |
| История | task_events: создана, статус, назначена, срок, приоритет, перенесена, привязка, отвязка |
| Привязки | строки links к сообщениям, чатам, людям, документам, заметкам и адресам, с видом |
Каждая задача принадлежит проекту. Задача из сообщения попадает во входящий проект своего аккаунта;
задача проекта — в проект, который создали вы. Привязка к сообщению имеет вид related-to,
evidence или created-from. Отношение между двумя задачами — тоже строка links, с видом
blocked-by, follow-up-of или related-to. Отношения работают только между задачами ваших
собственных проектов, и устанавливаете их только вы. Перед записью blocked-by или follow-up-of
рекурсивный запрос проверяет, что новая строка не замыкает цикл.
Планируется. Новые поля: category, claimed_by, claimed_until. Задача становится ещё одним
типом в существующей таблице taggings, поэтому команды и фильтры tags работают и на задачах. Новые виды
отношений: duplicate-of и discovered-from. Две новые полнотекстовые таблицы, task_words и
task_stems, устроенные как таблицы заметок. Каждое изменение категории становится строкой
task_events вида category.
Идентификаторы и ключи
Сейчас. У задачи проекта есть ключ из ключа проекта и номера: HRB-12. Номер берётся из
счётчика в строке проекта, в той же записи, что и вставка задачи:
UPDATE projects SET tasks_count = tasks_count + 1 WHERE id = ? RETURNING key, tasks_countУ задачи из сообщения есть внутренний id (UUID) и ключ из входящего проекта её аккаунта: IN,
8 шестнадцатеричных символов и номер, например IN1A2B3C4D-17.
tasks list показывает начало id; tasks show и tasks agenda показывают ключ.
Планируется. Новая задача из сообщения получает вместо ключа входящего проекта читаемый ключ по
типу: Q-15 вопрос, REQ-22 просьба, M-7 упоминание, PR-4 обещание. Старые задачи сохраняют
ключ IN…. У каждого типа своя строка-счётчик, которая берётся в той же
записи, что и вставка, как у проектов. UUID и оба вида ключей находятся как task:<…>. Категория — поле,
а не часть ключа, поэтому исправленная категория не ломает ключ, уже процитированный в чате или
заметке.
Почему не хеш-идентификаторы. beads использует
хеш-идентификаторы вроде bd-a3f8, которые удлиняются с ростом базы
(internal/idgen/hash.go).
Хеши решают задачу многих писателей на многих копиях, которые потом сливаются. Здесь одно хранилище
на компьютер и один писатель за раз, поэтому счётчик даёт короткие ключи, которые человек может
прочитать. Если хранилища когда-нибудь начнут синхронизироваться между устройствами, ключи получат
префикс устройства; это решение на потом.
Бронь (планируется)
Два агента или агент и вы не должны одновременно работать над одной задачей. tasks claim
выполняет один запрос внутри BEGIN IMMEDIATE:
UPDATE tasks SET claimed_by = ?, claimed_until = ?
WHERE id = ? AND status = 'open' AND (claimed_by IS NULL OR claimed_until < ?)
RETURNING idЕсли строка не вернулась, задачу держит кто-то другой; ошибка называет кто и до какого времени.
SQLite пускает только одного писателя за раз во всех процессах, поэтому бронь атомарна между tg,
max, memo и агентами на одном компьютере. Бронь — это аренда: она заканчивается сама (по
умолчанию через 30 минут), поэтому упавший агент не держит задачу вечно. tasks release снимает
бронь раньше; закрытие задачи снимает её тоже. tasks agenda скрывает задачи, забронированные
другими, если не попросить их через --claimed.
Проверка на дубликаты (планируется)
Проверка запускается для каждой новой задачи и для каждого нового сообщения, привязанного к задаче. Она работает в cli-core, на SQLite, без эмбеддингов и без обращения к провайдеру ИИ.
Словами: новая задача нормализуется; точное совпадение сразу привязывает её; иначе собираются кандидаты, каждый сравнивается по полям, веса складываются, и сумма решает: создать или спросить. Счёт никогда не объединяет две задачи; решает агент или человек. Каждый ответ сохраняется, чтобы потом подстроить веса.
1. Нормализация
- Название и описание проходят через нормализатор поиска: нижний регистр, без диакритики,
ёстановитсяе, группы цифр склеиваются (15 000становится15000). - Стеммер Snowball для каждой письменности (русский, английский, испанский) сводит каждое слово к основе; стоп-слова убираются. Получается набор основ.
- Нормализованное название также режется на символьные триграммы — все окна по три буквы. Они ловят опечатки и формы, которые стеммер пропускает.
- Шаблоны извлекают подпись: сумму с валютой (число рядом с
₽,руб,€,$,usd), срок (даты и фразы вродеby Fridayилидо пятницы) и человека (person:<id>автора исходного сообщения, черезidentities). Агент может передать их сам (--amount,--due,--person).
Основы и нормализованный текст попадают в task_words и task_stems, которые обновляются теми же
записями.
2. Точные совпадения
Без подсчёта баллов привязывает только одно совпадение: то же правило на том же сообщении, его держит уникальный индекс по источнику и правилу. Новая задача не создаётся, а в ответе сказано, к какой задаче и почему.
В первом варианте точными считались ещё ответ и то же название. На тестовом наборе они ошиблись 6 раз из 11: ответ с новой просьбой, чужое «+1», то же название для другого дела. Теперь ответ — поле с весом (шаг 4), а то же название — просто высокий балл текста.
3. Кандидаты
Кандидаты — объединение дешёвых выборок:
SELECT task_id FROM task_stems
WHERE task_stems MATCH '<stems joined by OR>'
ORDER BY bm25(task_stems) LIMIT 20плюс задачи того же человека, той же категории и той же цепочки ответов — обычные запросы по
индексам. Учитываются только открытые задачи и задачи, закрытые за последние 14 дней. В объединении
не больше примерно 40 задач. bm25() —
функция ранжирования SQLite FTS5.
Два правила не дают потерять хороших кандидатов:
- Один только общий чат никогда не делает кандидата. В чате много несвязанных задач, а одна просьба может прийти и в группу, и в личный чат.
- Одна выборка никогда не решает в одиночку. Graphiti терял точные дубликаты, потому что брал кандидатов из одного поиска с отсечкой (getzep/graphiti#1734).
4. Сравнение по полям
| Поле | Сравнение | Уровни |
|---|---|---|
| Текст | Жаккар (общие элементы, делённые на все различные) по наборам основ названия и описания и по триграммам названия; берётся большее | 0,6 и выше · от 0,3 до 0,6 · ниже 0,3 |
| Человек | person:<id> | тот же · другой · неизвестен |
| Сумма | значение и валюта | равны · разные · нет |
| Категория | название | та же · другая |
| Срок | разница в днях | 2 и меньше · больше · нет |
| Цепочка ответов | исходные сообщения отвечают друг на друга | да · нет |
| Время | дней между задачами | 3 и меньше · от 4 до 14 · больше |
5. Сумма весов
Счёт следует модели Феллеги — Сантера для связывания записей. У каждого уровня есть вес
log2(m / u). Здесь m — как часто этот уровень встречается у настоящих дубликатов, а u — у
несвязанных задач. Уровень, который у дубликатов обычен, поднимает сумму; уровень, который у них
редок, опускает её. Метод объясняет руководство Splink по модели Феллеги — Сантера.
Первые веса заданы вручную, затем подстроены на синтетическом тестовом наборе:
| Уровень | Вес |
|---|---|
| Текст: 0,6 и выше / от 0,3 до 0,6 / ниже 0,3 | +5 / +5 / −2 |
| Человек: тот же / другой / неизвестен | +1 / −3 / 0 |
| Сумма: равны / разные / нет | +6 / −4 / 0 |
| Категория: та же / другая | +1 / −1 |
| Срок: 2 дня и меньше / больше / нет | +4 / −1 / 0 |
| Цепочка ответов: да / нет | +6 / 0 |
| Время: 3 дня и меньше / от 4 до 14 дней / больше | +2 / −2 / −3 |
Ни у одного дубликата в наборе не было другого человека, суммы или категории, поэтому подсчёт дал бы этим уровням −5 или −6 без единого наблюдения. Они сохраняют веса, заданные вручную.
Полосы: 10 и больше — «вероятно», от 5 до 9 — «возможно», меньше 5 — «другая». «Вероятно» только ставит кандидата первым в списке; по счёту ничего не объединяется. Веса и полосы живут в одном модуле, с причиной для каждого числа; больше нигде они не зашиты.
Что показал тестовый набор
В наборе 251 синтетическая задача на русском и английском и 80 пар дубликатов. Используются настоящий
bm25() из FTS5 и стеммер cli-core; веса подстроены на одной половине и проверены на другой
(WireCatLabs/cli-testing#60).
- «Возможно» и «вероятно» вместе находят 90% дубликатов. 58% показанных пар — настоящие дубликаты, а 21% новых задач отклоняется до выбора.
- Одна полоса «вероятно» права в 85% случаев и находит 43% дубликатов.
- Ни один счёт не безопасен для автоматического объединения. Первый счёт без ошибочных объединений — 16, и он ловит 4% дубликатов. Оставшиеся похожие пары, например подарок маме и подарок папе на ту же сумму с разницей в два дня, отличаются смыслом одного слова, а его не видит ни одно поле.
Ограничения: данные синтетические, данные и оценку писал один автор, дубликаты на разных языках находятся плохо (2 из 5 получили «другая»). Измерений на настоящих чатах пока нет.
6. Решение
| Полоса | Задачу добавляет агент | Задачу открывает правило |
|---|---|---|
| Точное (шаг 2) | не применяется | ничего нового |
| Вероятно | показана первой; агент обязан выбрать | в список на проверку |
| Возможно | tasks add отказывает и показывает до 5 кандидатов со счётом и причинами; агент повторяет с --attach <task> или --new | в список на проверку |
| Другая | создаётся | создаётся |
Инструмент отказывает, а не предупреждает: предупреждение агент может пропустить, код выхода — нет. Спросить агента, который создаёт задачу, не стоит лишнего обращения к провайдеру ИИ: этот агент и так судья. Mem0 устроен так же: агент, который записывает воспоминание, выбирает — добавить его, обновить существующее, удалить или ничего не делать (Mem0, arXiv 2504.19413).
Список на проверку. tasks duplicates показывает пары, которые правило оставило в полосе
«вероятно» или «возможно», сначала новые, с причинами. tasks merge <dup> --into <task> привязывает все
сообщения дубликата к оставшейся задаче, закрывает дубликат как dismissed с отношением
duplicate-of и записывает решение. tasks duplicates reject <dup> <task> записывает «не одно и
то же», и пара больше не предлагается.
Объединение можно отменить. tasks unmerge <dup> снова открывает дубликат и убирает отношение.
Привязки остаются на месте, пока их не перенесут. Объединение никогда ничего не удаляет.
7. Учёт ответов
Каждое объединение, отказ, --attach и --new после списка кандидатов — размеченная пара. Когда
пар достаточно, m и u для каждого уровня пересчитываются по ним — та же оценка подсчётом, что в
Splink, без шага обучения, — и полосы сдвигаются. Счёт никогда не объединяет сам. Если настоящие
ответы покажут, что пары «вероятно» вы почти всегда принимаете, объединять ли их — решать вам.
Разобранный пример
Иван Пример пишет в личном чате: «Пришлёшь счёт на 15 000 ₽ до пятницы?» Правило открывает просьбу. До её создания проверка находит двух кандидатов от того же человека:
- REQ-22, создана вчера: «Send the invoice for 15000 ₽» (отправить счёт на 15000 ₽), категория
money, без срока. - REQ-19, создана 10 дней назад: «Send the contract invoice, 40 000 ₽» (отправить счёт по
договору), категория
money, без срока.
Названия задач записаны по-английски, поэтому работает английский стеммер. Новая задача названа
«Send the invoice for 15 000 ₽ by Friday»; её основы: send, invoic, 15000, friday. В этом примере триграммы дают меньше, чем основы, для обеих
пар, поэтому считаются основы.
| Поле | REQ-22 | Баллы | REQ-19 | Баллы |
|---|---|---|---|---|
| Текст | основы send, invoic, 15000: 3 общих из 4 различных = 0,75 | +5 | основы send, contract, invoic, 40000: 2 общих из 6 различных = 0,33 | +5 |
| Человек | тот же | +1 | тот же | +1 |
| Сумма | 15000 ₽ = 15000 ₽ | +6 | 15000 ₽ ≠ 40000 ₽ | −4 |
| Категория | money = money | +1 | money = money | +1 |
| Срок | нет у REQ-22 | 0 | нет у REQ-19 | 0 |
| Цепочка ответов | нет | 0 | нет | 0 |
| Время | 1 день | +2 | 10 дней | −2 |
| Итого | 15: вероятно | 1: другая |
Просьбу нашло правило, поэтому инструмент создаёт её как REQ-23 и кладёт пару REQ-23 и REQ-22 в
список на проверку. По счёту ничего не объединяется: агент или вы проверяете пару и запускаете
tasks merge REQ-23 --into REQ-22. Если бы задачу добавлял агент, tasks add отказал бы, показал
REQ-22 первой с 15 баллами и причиной «тот же человек, та же сумма, общих основ 3 из 4», и агент повторил бы команду с
--attach REQ-22. У REQ-19 тот же человек и те же слова «отправить счёт», но сумма и давность тянут
её вниз: это другое дело.
Сравнение с beads
beads — трекер задач для агентов-программистов. Часть его идей взята сюда, часть — нет.
| beads | Здесь |
|---|---|
Хеш-идентификаторы bd-a3f8 | Читаемые ключи из счётчика: одно хранилище, один писатель за раз |
| Метки для всего, что не поле | Одна категория из фиксированного списка плюс существующие теги |
Виды связей, например duplicates и discovered-from | duplicate-of и discovered-from добавляются к видам отношений |
bd ready: открытая работа без блокировок | tasks agenda; фильтры по категории и тегу планируются |
bd update --claim: атомарный захват | tasks claim — аренда, которая заканчивается сама |
external_ref на один внешний объект | Каждая задача привязана ко всем своим сообщениям |
bd duplicates объединяет точные копии; bd find-duplicates считает совпадение слов и только сообщает | Проверка до создания, веса полей и выбор агента или список на проверку |
Текст в чатах никогда не бывает точной копией, и ничто в beads не проверяет задачу до создания. Поэтому проверка здесь работает по полям и спрашивает, когда не уверена.
Похожие работы
Дубликаты отчётов об ошибках — старая задача. REP сравнивает два отчёта по тексту и по полям вроде продукта и компонента, с весами, подобранными на известных дубликатах (Sun et al., ASE 2011). Проверка здесь использует ту же идею — текст плюс поля, — но веса считаются, а не обучаются.
Чего нет
- Ни эмбеддингов, ни обращения к провайдеру ИИ для сравнения задач. Если тестовый набор покажет, что текстовое поле слишком слабое, следующий шаг — локальный эмбеддинг, измеренный против этой проверки.
- Никаких удалений. Дубликат закрывается и привязывается; его никогда не удаляют.
- Никакого ограничения «одна задача на чат или на человека».
- Никаких хеш-идентификаторов, пока хранилища не синхронизируются между устройствами.
- Открытые вопросы: список категорий по умолчанию и что делать с сообщением, похожим на задачу,
закрытую за последние 14 дней: снова открыть её или открыть новую со ссылкой
follow-up-of.