Drop unused thread_id from questions

This commit is contained in:
Ian Gulliver
2026-10-20 20:46:45 -07:00
parent 6ec3cafde5
commit e5a7fc4c1a
7 changed files with 319 additions and 339 deletions
+2 -2
View File
@@ -320,7 +320,7 @@ The **Project** tab is the dashboard of the right pane's project (the focused ta
## Inbox
Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript, and its choices are written inline in that context as Markdown list items: `- ( ) text` for pick-one choices, with exactly one `- (*) text` marking the recommendation, or `- [ ] text` for pick-any choices, with `- [x] text` marking the recommended ones. There is no separate list of options. byre parses the choices out of `context` (`internal/agents/choices.go`; list items inside code fences and blockquotes don't count) and stores their text in the payload as `options`, with `multiple` (pick-any) and `recommended`. The UI renders the context as Markdown with a radio button or checkbox in place of each choice's marker. The tool's description tells agents to keep everything about one choice (what it does, its example, code, trade-offs) in lines indented beneath that choice rather than in text above the list; those lines render inside the option, and only the marker line's text is the option. When every item of a choice list has exactly one non-blank indented line and that line is only a Markdown image, the list renders as a grid of tiles (`grid`, set in `parseList` in `mac/Sources/Byre/Markdown.swift`): four across, or two across when the context is at most 480pt wide. The tool's description tells agents to write image choices this way. A malformed call (no `context` or `question`, fewer than two choices, pick-one and pick-any choices mixed, a pick-one question without exactly one recommendation, two choices with the same text, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. All questions are alike: the tool's description and its result tell the agent to end its turn after asking, and the answer arrives as its next message. When the task's worktree is behind the default branch (`git.Behind`: commits on the repo's local default branch that `HEAD` lacks) and the turn isn't a Merge or Sync turn that owns the worktree's merge, byre first commits the worktree itself (`commitBeforeSync`: `git.CommitAll` under the task's mutex) and takes an `ask_human` snapshot of it, so the agent never commits on its own and there is a forkable point just before the merge. The result then says how many commits behind it is and suggests merging the default branch in while the human answers: `git merge --no-ff --no-commit`, resolve, check, and leave it uncommitted for byre's end-of-turn commit to conclude, as **Sync from main** does, then end the turn. When the commit or snapshot fails, it is logged as `commit before merging default branch` and recorded as a `session.error`, and the result just says to end the turn. The call returns the item's id as `question_id`. An agent withdraws a question it no longer needs answered (the human answered it in a chat message, say) with the `cancel_question` MCP tool and that id: an `open` question of its own session becomes `dismissed` with no `resolved_by` (its `inbox.updated` event has actor `agent`), which also stops it from holding back delivery; anything else is rejected with a message saying why. The inbox holds only questions; errors go in the transcript (see "Errors").
Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript, and its choices are written inline in that context as Markdown list items: `- ( ) text` for pick-one choices, with exactly one `- (*) text` marking the recommendation, or `- [ ] text` for pick-any choices, with `- [x] text` marking the recommended ones. There is no separate list of options. byre parses the choices out of `context` (`internal/agents/choices.go`; list items inside code fences and blockquotes don't count) and stores their text in the payload as `options`, with `multiple` (pick-any) and `recommended`. The UI renders the context as Markdown with a radio button or checkbox in place of each choice's marker. The tool's description tells agents to keep everything about one choice (what it does, its example, code, trade-offs) in lines indented beneath that choice rather than in text above the list; those lines render inside the option, and only the marker line's text is the option. When every item of a choice list has exactly one non-blank indented line and that line is only a Markdown image, the list renders as a grid of tiles (`grid`, set in `parseList` in `mac/Sources/Byre/Markdown.swift`): four across, or two across when the context is at most 480pt wide. The tool's description tells agents to write image choices this way. A malformed call (no `context` or `question`, fewer than two choices, pick-one and pick-any choices mixed, a pick-one question without exactly one recommendation, two choices with the same text, an unknown attachment kind) is rejected with a message saying what to fix, and nothing is filed. All questions are alike: the tool's description and its result tell the agent to end its turn after asking, and the answer arrives as its next message. When the task's worktree is behind the default branch (`git.Behind`: commits on the repo's local default branch that `HEAD` lacks) and the turn isn't a Merge or Sync turn that owns the worktree's merge, byre first commits the worktree itself (`commitBeforeSync`: `git.CommitAll` under the task's mutex) and takes an `ask_human` snapshot of it, so the agent never commits on its own and there is a forkable point just before the merge. The result then says how many commits behind it is and suggests merging the default branch in while the human answers: `git merge --no-ff --no-commit`, resolve, check, and leave it uncommitted for byre's end-of-turn commit to conclude, as **Sync from main** does, then end the turn. When the commit or snapshot fails, it is logged as `commit before merging default branch` and recorded as a `session.error`, and the result just says to end the turn. The call returns the item's id as `question_id`. An agent withdraws a question it no longer needs answered (the human answered it in a chat message, say) with the `cancel_question` MCP tool and that id: an `open` question of its own session becomes `dismissed` with no `resolved_by` (its `inbox.updated` event has actor `agent`), which also stops it from holding back delivery; anything else is rejected with a message saying why. The inbox holds only questions; errors go in the transcript (see "Errors").
`GET /api/inbox` is one query over open items, oldest first; `project_id` and `task_id` narrow it to a project or task view, and `state` picks another state. The right pane shows a task's questions in its transcript, where they were asked, and they are answered there (below).
@@ -372,7 +372,7 @@ Markdown images (`![alt](src)`, or `![alt](<src>)`; the plain form also accepts
A code block fenced as `mermaid`, and an inbox `diagram` attachment, render as a diagram drawn natively when the source is a flowchart (`mac/Sources/Byre/Diagram.swift`; `docs/macos.md`, "Diagrams", lists what it parses); any other source stays as source with "diagram not rendered: <reason>" under it. The image endpoint (`internal/server/images.go`) serves PNG, JPEG, GIF, WebP and SVG by extension, resolves symlinks on both the worktree and the file and answers 403 for anything outside the worktree, 404 for a missing file, 400 for another extension or a non-file, and 409 once the task is closed (its worktree is gone). Responses carry `X-Content-Type-Options: nosniff` and a sandboxing `Content-Security-Policy`, so an SVG opened directly runs no script, and `Cache-Control: no-cache` with `Last-Modified`, so a changed file is refetched.
Answers reach the agent as one message, listing each answer under the question it responds to, once the session is idle and none of its questions are still waiting for the human. An `other` answer to a question tells the agent that if it asks something back, it should answer with `ask_human` using the item's `thread_id`. `thread_id` only groups items: filing an item never changes any other, so several questions on one thread stay open until each is answered, withdrawn or dismissed. Nothing sets the `superseded` state; it stays in the spec because old items hold it. Every item change is an `inbox.created` or `inbox.updated` event carrying the whole item, including who resolved it.
Answers reach the agent as one message, listing each answer under the question it responds to, once the session is idle and none of its questions are still waiting for the human. Filing a question never changes any other, so several open questions stay open until each is answered, withdrawn or dismissed. Nothing sets the `superseded` state; it stays in the spec because old items hold it. Every item change is an `inbox.created` or `inbox.updated` event carrying the whole item, including who resolved it.
## Client Macs