Skip to content

feat(tools): add b24_task_comment_list - #260

Open
aa-blinov wants to merge 2 commits into
bitrix24:mainfrom
aa-blinov:feat/task-comment-list
Open

aa-blinov wants to merge 2 commits into
bitrix24:mainfrom
aa-blinov:feat/task-comment-list

Conversation

@aa-blinov

Copy link
Copy Markdown

Summary

Adds b24_task_comment_list — the read side of task comments. The template could write them (b24_task_comment_add) but never read them, so an agent could not answer "who said what on this task". Wraps task.commentitem.getlist (v2), returns every comment in full with authorId + authorName, an authors roll-up, and total / matched / returned counters.

Type of change

  • feat
  • fix
  • docs
  • chore
  • test
  • refactor
  • ci

Linked issue

Checklist

  • PR title follows Conventional Commits
  • pnpm lint passes
  • pnpm typecheck passes
  • pnpm test passes
  • New or changed code has tests
  • Public-facing changes reflected in docs/ and skills/ (README tool table + docs/MANUAL-TEST-PHRASES.md §6; the tool-authoring conventions are unchanged, so docs/ADDING-TOOLS.md / the agent skill need no edit — only the delete-tool registry lives there and this tool is read-only)
  • No unrelated changes
  • No secrets in code, tests, or CI logs

Screenshots / logs

Verified against a live portal (webhook auth) before opening this PR — a task with a mixed thread:

{"taskId":29,"total":4,"matched":4,"returned":4,"offset":0,
 "authors":[{"id":9,"name":"…","comments":3},{"id":11,"name":"…","comments":1}],
 "comments":[{"id":71,"taskId":29,"authorId":9,"authorName":"…","authorEmail":null,
              "postDate":"2025-07-31T13:39:26+03:00","text":"[USER=11]…[/USER], вы назначены соисполнителем.","textHtml":null}, …]}

Author names / bodies elided here; the fixtures in the test suite are synthetic.

Notes for reviewers

Three findings from the live portal that shaped the design — each is the reason for a piece of the implementation, so they're worth a look before reviewing the code:

  1. The endpoint accepts TASKID only. Adding ORDER (and likewise FILTER) fails the whole call with ERROR_CORE / TASKS_ERROR_EXCEPTION_#8 … ACTION_FAILED_TO_BE_PROCESSED — not a silently-ignored param. Ordering, the authorId filter and limit / offset paging are therefore applied locally over the full thread. One round-trip; threads are tens of comments. The tool header comments say so explicitly to stop a future "optimisation" from pushing params onto the wire.

  2. tasks.task.chat.message.list is not a usable replacement today — it returns an empty body on webhook auth, so the classic (deprecated) method stays. Worth knowing for feat(tools): add task CRUD — create / list / update / add-comment #4 on the roadmap in docs/MANUAL-TEST-PHRASES.md, which plans the same migration for b24_task_comment_add.

  3. Service messages cannot be filtered the way §6 of MANUAL-TEST-PHRASES.md assumed. That doc specified an includeSystem: false default keyed on messageType: "SERVICE" / AUTHOR_ID: 0. Neither field exists on this endpoint: Bitrix24's lifecycle notes ("Задача завершена.", "Крайний срок изменен на: …", "[USER=N]…[/USER], вы назначены соисполнителем.") arrive as ordinary comments authored by the user who triggered the change, indistinguishable at the REST layer from something that person typed. I chose to return them verbatim and tell the agent in the description to judge by the text, rather than ship a text heuristic that would mislabel a genuine comment that happens to read like a template. §6 is rewritten to record this. If you'd rather have the heuristic behind an opt-in flag, say so and I'll add it — but I'd argue the false positives are worse than the noise, since the failure mode is quoting a template back to the operator as a human statement.

Two smaller deliberate choices:

  • Bodies are never truncated. limit / offset drop whole comments; no comment is ever shortened. A cut-off comment silently changes what the operator is told.
  • total vs matched vs returned — thread size / after the author filter / after paging. Lets the agent distinguish "no comments" from "filter matched nothing" and detect further pages, in the same spirit as the returned-vs-limit idiom the v3 list tools use.

Transport is v2 per the convention block in sdk-helpers.ts (classic task.* method). Tests: 14 unit cases across the parser and the tool (ordering, author filter, paging, empty thread, legacy { result: [...] } envelope, error wrap), plus 3 eval cases including read-vs-add and read-vs-b24_task_result_list disambiguation.

The template could write task comments (`b24_task_comment_add`) but never
read them, so an agent could not answer "who said what on this task".

`b24_task_comment_list` wraps `task.commentitem.getlist` (v2) and returns
every comment in full — bodies are never truncated — with `authorId` +
`authorName` per comment (Bitrix24 ships both, so attribution costs no
extra `user.get`), an `authors` roll-up over the whole thread, and a
`total` / `matched` / `returned` triple so the agent can tell "no
comments" from "the filter matched nothing" and detect further pages.

Wire constraints, verified against a live portal (2026-09-02):

- The endpoint accepts `TASKID` only. Passing `ORDER` (or `FILTER`) fails
  the whole call with `ERROR_CORE` /
  `TASKS_ERROR_EXCEPTION_#8 … ACTION_FAILED_TO_BE_PROCESSED`, so ordering,
  the `authorId` filter and `limit` / `offset` paging are applied locally
  over the full thread. Threads are tens of comments, so the single
  round-trip stays cheap.
- `tasks.task.chat.message.list` was evaluated as the modern replacement
  and rejected: it returns an empty body on webhook auth.
- `POST_MESSAGE_HTML` is null for UI-written comments; `POST_MESSAGE`
  (BBCode) is the canonical body.

Service messages are returned as-is. `docs/MANUAL-TEST-PHRASES.md` section 6
specified an `includeSystem: false` default keyed on
`messageType: "SERVICE"` / `AUTHOR_ID: 0`, but neither field exists —
Bitrix24's lifecycle notes are ordinary comments authored by whoever
triggered the change, indistinguishable at the REST layer from something
that person typed. The tool surfaces them verbatim and the description
tells the agent to judge by the text, rather than mislabelling a real
comment with a heuristic. Section 6 of that doc is rewritten to record
what the API actually does.

Adds `server/utils/task-comments.ts` (`toTaskCommentShort`, sibling to the
elapsed-time / checklist / result parsers), `BitrixTaskCommentRaw`, the
`mcp-stdio/tools.ts` registry entry, 14 unit tests, and 3 eval cases
(read-vs-add and read-vs-`b24_task_result_list` disambiguation).
@aa-blinov

Copy link
Copy Markdown
Author

I only found #119 after opening this, sorry — it covers the same ground, so: Refs #119.

This PR is the read half of it. On the two things that issue leaves open:

tasks.task.chat.message.list doesn't exist as a usable read path today, at least on webhook auth — it answers with an empty body, no error, nothing. So the fallback in the issue ("if no read method exists, document it as a hard API gap") turned out to be the situation for v3.

The other half is more useful, I think: task.commentitem.getlist does work on modern task cards. I read threads on tasks created this summer on a current portal, including ones with 4-digit ids, and got the comments back fine. So the "не работает в новой карточке задач" note in the issue doesn't hold for webhook auth on a live portal — which is why I went with v2 here instead of documenting a gap.

One quirk worth knowing before you review: the endpoint takes TASKID and nothing else. Pass ORDER and the whole call dies with ERROR_CORE / ACTION_FAILED_TO_BE_PROCESSED, not a silently ignored param. That's why sorting, the author filter and paging all happen locally on the full thread.

I deliberately left the send migration out — the authorId / on-behalf semantics differ enough from the v2 AUTHOR_ID field that it deserves its own PR and its own permission testing. Happy to pick that up separately if you want it.

Last thing, and this is the one place I went against what the docs asked for. MANUAL-TEST-PHRASES.md §6 specified includeSystem: false by default, filtering on messageType: "SERVICE" / AUTHOR_ID: 0. Neither field exists on this endpoint. Bitrix24's own lifecycle notes ("Задача завершена.", "Крайний срок изменен на: …") come back as ordinary comments authored by whoever triggered the change — there's nothing to filter on. I chose to return them as-is and warn the agent in the tool description rather than guess from the text, because the failure mode of a heuristic is quoting a template back to the operator as something a person actually said. If you'd rather have it behind an opt-in flag anyway, say so and I'll add it.

…forum

My first cut of `b24_task_comment_list` was wrong in the way that matters
most: it answered "no comments" with total confidence for any task
created recently.

Bitrix24 keeps task comments in two places and the task's age decides
which one:

  - legacy tasks carry a `forumTopicId`; comments are forum posts, read
    with `task.commentitem.getlist`;
  - tasks created after the portal moved to the chat-based task card
    carry a `chatId` and no forum topic; comments are chat messages, read
    with `im.dialog.messages.get` (DIALOG_ID: "chat<id>").

For a chat-era task the forum method returns `[]` — no error, no hint.
Verified live (2026-09-03): a task created that day reported
`forumTopicId: null` / `chatId: 6479`, two comments posted through
`b24_task_comment_add` came back only from the chat, and the tool as
first written reported an empty thread. This is exactly the failure the
issue description warned about with "не работает в новой карточке задач";
my earlier claim that the forum method works on modern task cards was
drawn from 2025 tasks and does not generalise.

A task straddling the migration holds comments in BOTH stores — the 2025
task's own chat contained two system notices, one of them telling the
reader that earlier comments stay in the forum — so the tool now reads
both, merges them chronologically, and tags each comment with
`source: "forum" | "chat"`.

Two things fall out of the chat store:

  - `author_id: 0` marks a Bitrix24-generated entry, so the
    `includeSystem: false` default that `MANUAL-TEST-PHRASES.md` §6
    always specified is finally implementable (hidden entries counted in
    `systemHidden`). It reaches chat entries only; the forum API has no
    marker, so a forum row reports `isSystem: false` meaning "unknown",
    and the description says so.
  - the response ships a `users` array, so chat-side attribution needs no
    extra `user.get`, matching what AUTHOR_NAME gives us on the forum
    side.

Chat paging walks backwards through `LAST_ID` at 200 per call, up to five
pages, and reports `chatTruncated: true` if the thread is longer.
Response also carries per-store counts. Bodies are still returned in
full; `limit` / `offset` drop whole comments.

Live re-check: the QA task returns its 2 human comments with 17 system
entries hidden and counted; the 2025 task returns 4 forum comments plus
its 2 chat system notices; §6 of the manual-test doc is rewritten around
the two-store reality.
@aa-blinov

Copy link
Copy Markdown
Author

I have to correct myself on the main claim I made above, and it's the one that matters.

I said task.commentitem.getlist works on modern task cards, so the "не работает в новой карточке задач" note in #119 didn't hold. That was wrong. I had tested it against 2025 tasks, which are forum-era, and generalised from them. Your note was right.

What actually happens: Bitrix24 keeps task comments in two places, and the task's age decides which. Legacy tasks carry a forumTopicId and their comments are forum posts. Tasks created since the portal moved to the chat-based task card carry a chatId, forumTopicId: null, and their comments are chat messages — and for those, commentitem.getlist returns []. No error, no hint, just an empty thread. I created a task yesterday to check, posted two comments through b24_task_comment_add, and the tool as I first wrote it told me there were none. That's the worst possible answer.

So I've reworked the PR. It now reads both stores and merges them, because a task from around the migration holds comments in both — the 2025 task I tested has four forum comments and its chat contains exactly two system notices, one of which literally tells the reader that earlier comments stay in the forum. Each comment carries source: "forum" | "chat".

The read path for the chat is im.dialog.messages.get with DIALOG_ID: "chat<chatId>". Only needs the im scope, which is in the standard webhook set. tasks.task.chat.message.list is still a dead end — empty body on webhook auth — so #119's acceptance question has an answer now: a working read path exists, just not the v3 one.

Two nice consequences of the chat store:

author_id: 0 marks Bitrix24's own entries. So the includeSystem: false default that §6 of MANUAL-TEST-PHRASES.md specified all along is implementable after all, and I've implemented it — hidden entries are counted in systemHidden. My earlier argument that system messages can't be told apart holds only for the forum store, which has no marker; a source: "forum" row reports isSystem: false meaning "unknown", and the tool description says exactly that.

The chat response also ships a users array, so chat-side names come free, matching what AUTHOR_NAME gives on the forum side.

Chat paging walks backwards through LAST_ID, 200 per call, five pages max, and sets chatTruncated: true if the thread runs longer. Bodies are still never truncated.

Sorry for the noise of a wrong claim followed by a correction — better here than in merged code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant