main · last commit
13 days ago ·
7g0stsfu
ah-ptu Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5)
Past Stand
bd reopen ah-ptu
| Created by | Eugene Blikh |
| Owner | bigbes@gmail.com |
| Created | 2026-07-18T15:04:41Z |
| Started | 2026-07-18T18:19:54Z |
| Updated | 2026-07-18T18:47:53Z |
| Closed | 2026-07-18T18:47:53Z |
Why: the operator wants to ask questions as comments on Yonote docs (e.g. on agent-published reports) and have the bot answer in-thread. What: a polling loop (daemon has no public ingress — loopback-only listen — so webhooks are out for now) over events.list filtered to comments.create since a persisted cursor; qualifying comments spawn a question-answer board card whose description carries the doc URL (ah-2lh materialization then feeds the agent the doc as context); the answer is delivered via comments.create (parentCommentId threading) + optional comments.resolve under the bot identity. EXPLICITLY NOT wave 2.5: needs cursor persistence, a second poll loop, trigger-convention + mention-encoding verification, and a reply-channel decision. Depends on ah-gxa (comments methods; extend with ListEvents when picked up) and ah-25e (reply channel + bot write provisioning).
# Yonote Q&A bot loop (post-wave-2.5 — honest scope, NOT part of wave 2.5)
Goal: the operator asks questions IN Yonote (comments on docs — e.g. on agent-published reports
or on specs); the bot notices, an agent produces the answer, the bot replies in-thread and
resolves the comment.
## Verified surface this builds on (2026-07-18, bigbes.yonote.ru 1.47.1)
- comments.list {entityId, isResolved filter, threadId, limit ≤100, offset} → {data:{comments:[…]},
pagination, total} — LIVE-verified (nested envelope; items carry threadId, isResolved,
threadCommentsCount, attachments).
- comments.create {entityType:"document", entityId, text, parentCommentId} → {data:{comment}} —
LIVE-verified end-to-end on a probe doc: text stored verbatim (plain string), threading via
parentCommentId; a reply's threadId points at the root comment.
- comments.resolve {id, isResolved} → data.comment — spec'd (v2-preview), plus the product MCP
server exposes yonote_resolve_comment, so the surface is real.
- events.list POST {name, actorId, documentId, collectionId, limit, offset, sort/direction} —
LIVE-verified (returned {pagination, data:[{id, name, actorId, actor, documentId, collectionId,
createdAt, data, modelId}]}); the `name` filter ("objects.verb", e.g. "comments.create") is
spec'd — server-side event filtering makes cheap incremental polling possible.
- webhookSubscriptions.list responds LIVE (empty list) though it is UNDOCUMENTED in both Yonote
OpenAPI specs (Outline heritage; auth.info policies include create/listWebhookSubscription).
BUT the daemon binds loopback-only (config validateLoopback) with no public ingress, so v1
transport = POLLING events.list; webhooks stay a documented future option if ingress appears.
## Sketch (design decisions OPEN — settle when picked up)
- Poll loop: own ticker (~60s), separate from the reconcile tick; cursor persisted in the store
(new kv row, e.g. yonote_cursor = last seen event id/createdAt). MUST verify then: events.list
ordering + Sorting params for reliable incremental reads (direction/sort exist in the Pagination
/Sorting schemas but were not exercised).
- Trigger filter (pick one): (a) new unresolved comments on docs AUTHORED BY THE BOT
(createdById == bot user id from AuthInfo) — simplest, covers "questions about agent output";
(b) explicit @mention of the bot — mention ENCODING in comment text is UNVERIFIED (probe stored
plain text verbatim; the editor may encode mentions as structured nodes invisible to .text);
(c) any unresolved comment in configured collections.
- Self-trigger guard: ignore events with actorId == bot user id.
- Answer path: reuse the whole existing pipeline — create a board card (existing CreateTask port,
agent_tasks bucket conventions) of a question-answer task type whose description contains the
doc URL (the materialization bead then auto-exports the doc as context for the agent!) plus the
question thread text; the agent writes the answer; the daemon replies via comments.create with
parentCommentId = thread root and optionally comments.resolve. Reply delivery needs either a
small extension of the publish channel ({"reply_to_comment": …}) or a dedicated
.task/answer.json — decide at design time.
## Why not wave 2.5
Needs cursor persistence, a second poll loop, trigger-convention and mention-encoding
verification, and a reply file-channel decision — none of which artifact materialization or the
publish lane depend on. Client extensions required when picked up: ListEvents (events.list) and
possibly webhookSubscriptions.* — add to internal/yonote then, not now.
Depends on: internal/yonote client bead (comments methods land there), publish-lane bead (reply
channel shape + bot write provisioning).
Live evidence (2026-07-18): events.list POST works — {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}, event names 'objects.verb' (observed documents.permanent_delete from the trash-purge cron); the name/actorId/documentId/collectionId request filters are v1-spec'd. comments.create/list verified end-to-end on the probe doc (threading via parentCommentId; replies carry threadId of the root; text stored verbatim). comments.resolve spec'd + present as yonote_resolve_comment in the product's own MCP server. webhookSubscriptions.list responds live (empty, UNDOCUMENTED in both OpenAPI specs — Outline heritage; auth.info policies include createWebhookSubscription) — future option only, blocked on public ingress. UNVERIFIED for pickup: events.list ordering/Sorting params for reliable incremental cursors; @mention encoding inside comment text (probe stored plain text; the editor may use structured nodes invisible in .text — trigger option (a) 'comments on bot-authored docs' avoids the question entirely).
ah-eje
— Q&A loop rollout: live-verify events.list ordering + comments.resolve, wire qa role/config on agent-1
blocks
ah-gxa
— internal/yonote: light API client (doc resolve/export, create, comments; bot-token auth)
blocks
closed
ah-25e
— Yonote publish lane: .task/publish.json → bot-authored docs at finalize
blocks
closed
ah-2lh
— Claim-time Yonote artifact materialization into .task/artifacts/ + prompt manifest
blocks
closed
| id | ah-ptu |
| content_hash | 656fcc1b3e50d2d0c581f01969cc197111ed13fffaf60d43d5c82a961ec5f638 |
| title | Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5) |
| description | Why: the operator wants to ask questions as comments on Yonote docs (e.g. on agent-published reports) and have the bot answer in-thread. What: a polling loop (daemon has no public ingress — loopback-only listen — so webhooks are out for now) over events.list filtered to comments.create since a persisted cursor; qualifying comments spawn a question-answer board card whose description carries the doc URL (ah-2lh materialization then feeds the agent the doc as context); the answer is delivered via comments.create (parentCommentId threading) + optional comments.resolve under the bot identity. EXPLICITLY NOT wave 2.5: needs cursor persistence, a second poll loop, trigger-convention + mention-encoding verification, and a reply-channel decision. Depends on ah-gxa (comments methods; extend with ListEvents when picked up) and ah-25e (reply channel + bot write provisioning). |
| design | # Yonote Q&A bot loop (post-wave-2.5 — honest scope, NOT part of wave 2.5) Goal: the operator asks questions IN Yonote (comments on docs — e.g. on agent-published reports or on specs); the bot notices, an agent produces the answer, the bot replies in-thread and resolves the comment. ## Verified surface this builds on (2026-07-18, bigbes.yonote.ru 1.47.1) - comments.list {entityId, isResolved filter, threadId, limit ≤100, offset} → {data:{comments:[…]}, pagination, total} — LIVE-verified (nested envelope; items carry threadId, isResolved, threadCommentsCount, attachments). - comments.create {entityType:"document", entityId, text, parentCommentId} → {data:{comment}} — LIVE-verified end-to-end on a probe doc: text stored verbatim (plain string), threading via parentCommentId; a reply's threadId points at the root comment. - comments.resolve {id, isResolved} → data.comment — spec'd (v2-preview), plus the product MCP server exposes yonote_resolve_comment, so the surface is real. - events.list POST {name, actorId, documentId, collectionId, limit, offset, sort/direction} — LIVE-verified (returned {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}); the `name` filter ("objects.verb", e.g. "comments.create") is spec'd — server-side event filtering makes cheap incremental polling possible. - webhookSubscriptions.list responds LIVE (empty list) though it is UNDOCUMENTED in both Yonote OpenAPI specs (Outline heritage; auth.info policies include create/listWebhookSubscription). BUT the daemon binds loopback-only (config validateLoopback) with no public ingress, so v1 transport = POLLING events.list; webhooks stay a documented future option if ingress appears. ## Sketch (design decisions OPEN — settle when picked up) - Poll loop: own ticker (~60s), separate from the reconcile tick; cursor persisted in the store (new kv row, e.g. yonote_cursor = last seen event id/createdAt). MUST verify then: events.list ordering + Sorting params for reliable incremental reads (direction/sort exist in the Pagination /Sorting schemas but were not exercised). - Trigger filter (pick one): (a) new unresolved comments on docs AUTHORED BY THE BOT (createdById == bot user id from AuthInfo) — simplest, covers "questions about agent output"; (b) explicit @mention of the bot — mention ENCODING in comment text is UNVERIFIED (probe stored plain text verbatim; the editor may encode mentions as structured nodes invisible to .text); (c) any unresolved comment in configured collections. - Self-trigger guard: ignore events with actorId == bot user id. - Answer path: reuse the whole existing pipeline — create a board card (existing CreateTask port, agent_tasks bucket conventions) of a question-answer task type whose description contains the doc URL (the materialization bead then auto-exports the doc as context for the agent!) plus the question thread text; the agent writes the answer; the daemon replies via comments.create with parentCommentId = thread root and optionally comments.resolve. Reply delivery needs either a small extension of the publish channel ({"reply_to_comment": …}) or a dedicated .task/answer.json — decide at design time. ## Why not wave 2.5 Needs cursor persistence, a second poll loop, trigger-convention and mention-encoding verification, and a reply file-channel decision — none of which artifact materialization or the publish lane depend on. Client extensions required when picked up: ListEvents (events.list) and possibly webhookSubscriptions.* — add to internal/yonote then, not now. Depends on: internal/yonote client bead (comments methods land there), publish-lane bead (reply channel shape + bot write provisioning). |
| acceptance_criteria | |
| notes | Live evidence (2026-07-18): events.list POST works — {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}, event names 'objects.verb' (observed documents.permanent_delete from the trash-purge cron); the name/actorId/documentId/collectionId request filters are v1-spec'd. comments.create/list verified end-to-end on the probe doc (threading via parentCommentId; replies carry threadId of the root; text stored verbatim). comments.resolve spec'd + present as yonote_resolve_comment in the product's own MCP server. webhookSubscriptions.list responds live (empty, UNDOCUMENTED in both OpenAPI specs — Outline heritage; auth.info policies include createWebhookSubscription) — future option only, blocked on public ingress. UNVERIFIED for pickup: events.list ordering/Sorting params for reliable incremental cursors; @mention encoding inside comment text (probe stored plain text; the editor may use structured nodes invisible in .text — trigger option (a) 'comments on bot-authored docs' avoids the question entirely). |
| status | closed |
| priority | 3 |
| issue_type | feature |
| assignee | Eugene Blikh |
| estimated_minutes | NULL |
| created_at | 2026-07-18T15:04:41Z |
| created_by | Eugene Blikh |
| owner | bigbes@gmail.com |
| updated_at | 2026-07-18T18:47:53Z |
| closed_at | 2026-07-18T18:47:53Z |
| closed_by_session | |
| external_ref | NULL |
| spec_id | |
| compaction_level | 0 |
| compacted_at | NULL |
| compacted_at_commit | NULL |
| original_size | NULL |
| sender | |
| ephemeral | 0 |
| wisp_type | |
| pinned | 0 |
| is_template | 0 |
| mol_type | |
| work_type | |
| source_system | |
| metadata | �{} |
| source_repo | |
| close_reason | Implemented: events.list poll loop w/ kv cursor + exactly-once spawn, threaded answer delivery via answer.json, config yonote.qa block, store v5, prompts/qa.md. Rollout/live-verify tracked in ah-eje. |
| event_kind | |
| actor | |
| target | |
| payload | |
| await_type | |
| await_id | |
| timeout_ns | 0 |
| waiters | |
| hook_bead | |
| role_bead | |
| agent_state | |
| last_activity | NULL |
| role_type | |
| rig | |
| due_at | NULL |
| defer_until | NULL |
| no_history | 0 |
| started_at | 2026-07-18T18:19:54Z |
| is_blocked | 0 |
| id | 22775d24-27d7-51cb-b356-4a769bb6c58b |
| issue_id | ah-eje |
| type | blocks |
| created_at | 2026-07-18T21:47:52Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-ptu |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 6a912c84-0b5c-5b5f-9646-941067cace98 |
| issue_id | ah-ptu |
| type | blocks |
| created_at | 2026-07-18T18:04:57Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-gxa |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 76375cfe-53a8-5c6d-ba68-4de57738ddc7 |
| issue_id | ah-ptu |
| type | blocks |
| created_at | 2026-07-18T18:04:57Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-25e |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 019f75c2-13bc-7b1f-b19a-c3f9ccc31d01 |
| issue_id | ah-ptu |
| event_type | created |
| actor | Eugene Blikh |
| old_value | |
| new_value | |
| comment | NULL |
| created_at | 2026-07-18T18:04:41Z |
| id | 019f7674-cd39-7b71-b8aa-6e354dfe3552 |
| issue_id | ah-ptu |
| event_type | claimed |
| actor | Eugene Blikh |
| old_value | {"id":"ah-ptu","title":"Yonote Q\u0026A bot loop: poll doc comments, answer via board tasks (post-wave-2.5)","description":"Why: the operator wants to ask questions as comments on Yonote docs (e.g. on agent-published reports) and have the bot answer in-thread. What: a polling loop (daemon has no public ingress — loopback-only listen — so webhooks are out for now) over events.list filtered to comments.create since a persisted cursor; qualifying comments spawn a question-answer board card whose description carries the doc URL (ah-2lh materialization then feeds the agent the doc as context); the answer is delivered via comments.create (parentCommentId threading) + optional comments.resolve under the bot identity. EXPLICITLY NOT wave 2.5: needs cursor persistence, a second poll loop, trigger-convention + mention-encoding verification, and a reply-channel decision. Depends on ah-gxa (comments methods; extend with ListEvents when picked up) and ah-25e (reply channel + bot write provisioning).","design":"# Yonote Q\u0026A bot loop (post-wave-2.5 — honest scope, NOT part of wave 2.5)\n\nGoal: the operator asks questions IN Yonote (comments on docs — e.g. on agent-published reports\nor on specs); the bot notices, an agent produces the answer, the bot replies in-thread and\nresolves the comment.\n\n## Verified surface this builds on (2026-07-18, bigbes.yonote.ru 1.47.1)\n\n- comments.list {entityId, isResolved filter, threadId, limit ≤100, offset} → {data:{comments:[…]},\n pagination, total} — LIVE-verified (nested envelope; items carry threadId, isResolved,\n threadCommentsCount, attachments).\n- comments.create {entityType:\"document\", entityId, text, parentCommentId} → {data:{comment}} —\n LIVE-verified end-to-end on a probe doc: text stored verbatim (plain string), threading via\n parentCommentId; a reply's threadId points at the root comment.\n- comments.resolve {id, isResolved} → data.comment — spec'd (v2-preview), plus the product MCP\n server exposes yonote_resolve_comment, so the surface is real.\n- events.list POST {name, actorId, documentId, collectionId, limit, offset, sort/direction} —\n LIVE-verified (returned {pagination, data:[{id, name, actorId, actor, documentId, collectionId,\n createdAt, data, modelId}]}); the `name` filter (\"objects.verb\", e.g. \"comments.create\") is\n spec'd — server-side event filtering makes cheap incremental polling possible.\n- webhookSubscriptions.list responds LIVE (empty list) though it is UNDOCUMENTED in both Yonote\n OpenAPI specs (Outline heritage; auth.info policies include create/listWebhookSubscription).\n BUT the daemon binds loopback-only (config validateLoopback) with no public ingress, so v1\n transport = POLLING events.list; webhooks stay a documented future option if ingress appears.\n\n## Sketch (design decisions OPEN — settle when picked up)\n\n- Poll loop: own ticker (~60s), separate from the reconcile tick; cursor persisted in the store\n (new kv row, e.g. yonote_cursor = last seen event id/createdAt). MUST verify then: events.list\n ordering + Sorting params for reliable incremental reads (direction/sort exist in the Pagination\n /Sorting schemas but were not exercised).\n- Trigger filter (pick one): (a) new unresolved comments on docs AUTHORED BY THE BOT\n (createdById == bot user id from AuthInfo) — simplest, covers \"questions about agent output\";\n (b) explicit @mention of the bot — mention ENCODING in comment text is UNVERIFIED (probe stored\n plain text verbatim; the editor may encode mentions as structured nodes invisible to .text);\n (c) any unresolved comment in configured collections.\n- Self-trigger guard: ignore events with actorId == bot user id.\n- Answer path: reuse the whole existing pipeline — create a board card (existing CreateTask port,\n agent_tasks bucket conventions) of a question-answer task type whose description contains the\n doc URL (the materialization bead then auto-exports the doc as context for the agent!) plus the\n question thread text; the agent writes the answer; the daemon replies via comments.create with\n parentCommentId = thread root and optionally comments.resolve. Reply delivery needs either a\n small extension of the publish channel ({\"reply_to_comment\": …}) or a dedicated\n .task/answer.json — decide at design time.\n\n## Why not wave 2.5\n\nNeeds cursor persistence, a second poll loop, trigger-convention and mention-encoding\nverification, and a reply file-channel decision — none of which artifact materialization or the\npublish lane depend on. Client extensions required when picked up: ListEvents (events.list) and\npossibly webhookSubscriptions.* — add to internal/yonote then, not now.\n\nDepends on: internal/yonote client bead (comments methods land there), publish-lane bead (reply\nchannel shape + bot write provisioning).\n","notes":"Live evidence (2026-07-18): events.list POST works — {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}, event names 'objects.verb' (observed documents.permanent_delete from the trash-purge cron); the name/actorId/documentId/collectionId request filters are v1-spec'd. comments.create/list verified end-to-end on the probe doc (threading via parentCommentId; replies carry threadId of the root; text stored verbatim). comments.resolve spec'd + present as yonote_resolve_comment in the product's own MCP server. webhookSubscriptions.list responds live (empty, UNDOCUMENTED in both OpenAPI specs — Outline heritage; auth.info policies include createWebhookSubscription) — future option only, blocked on public ingress. UNVERIFIED for pickup: events.list ordering/Sorting params for reliable incremental cursors; @mention encoding inside comment text (probe stored plain text; the editor may use structured nodes invisible in .text — trigger option (a) 'comments on bot-authored docs' avoids the question entirely).","status":"open","priority":3,"issue_type":"feature","owner":"bigbes@gmail.com","created_at":"2026-07-18T15:04:41Z","created_by":"Eugene Blikh","updated_at":"2026-07-18T15:04:41Z"} |
| new_value | {"assignee":"Eugene Blikh","status":"in_progress"} |
| comment | NULL |
| created_at | 2026-07-18T21:19:54Z |
| id | 019f768e-6c3c-7213-b14d-2bbf0b22157c |
| issue_id | ah-ptu |
| event_type | closed |
| actor | Eugene Blikh |
| old_value | |
| new_value | Implemented: events.list poll loop w/ kv cursor + exactly-once spawn, threaded answer delivery via answer.json, config yonote.qa block, store v5, prompts/qa.md. Rollout/live-verify tracked in ah-eje. |
| comment | NULL |
| created_at | 2026-07-18T21:47:53Z |
| id | 019f7cda-aa8c-777a-b3da-2dda34bda058 |
| issue_id | ah-ptu |
| event_type | label_added |
| actor | Eugene Blikh |
| old_value | NULL |
| new_value | NULL |
| comment | Added label: milestone:yonote |
| created_at | 2026-07-20T03:08:53Z |
| id | 019f7cdc-29dd-7683-8ecf-896151103c2e |
| issue_id | ah-ptu |
| event_type | label_removed |
| actor | Eugene Blikh |
| old_value | NULL |
| new_value | NULL |
| comment | Removed label: milestone:yonote |
| created_at | 2026-07-20T03:10:31Z |
No comments.
Close reason