9 Commits
Author SHA1 Message Date
3191de1930 MUL-8385 feat(wecom): show the run's steps and reasoning in the bubble while it works (#9696)
* feat(wecom): show the run's steps and reasoning in the bubble while it works

The bubble used to open, spin for the whole run and close with the answer.
It now fills in while the run is going: each tool call as a sentence in the
reader's language, the agent's own reasoning under it, and how long it has
been. Only the bot's principal sees any of it, and only in their own 1:1 —
a group or anyone else gets the plain spinner, decided again for every round.

- Steps come from task:message (the transcript), plus the daemon's first
  task:progress milestone, which is said as "starting the agent" in the
  bubble's language rather than the daemon's English. Only runs asked over
  WeCom (a WeCom channel_task_delivery row) show steps.
- Progress reaches the replica that holds the round, whichever replica
  served the daemon's request. The bus subscriber only classifies the event
  and queues it on a per-run lane, so the daemon's request does no database
  read and no socket write. The holder reads the run's new transcript rows
  past a per-run watermark (ListTaskProgressSince), redacts, throttles and
  writes the frame. A replica without the round publishes a progress relay
  frame that carries ids and a seq, no text; it takes no claim and never
  enters the dispatcher's queues, so it cannot delay or crowd out an answer.
  Duplicates, replays and out-of-order frames read nothing new, and do not
  count as a sign of life for the rotation guard.
- Credentials are redacted from steps and reasoning: labelled values, known
  key prefixes, long high-entropy runs, and the same checks on each part of
  a URL or of a word glued to shell punctuation, plus webhook URLs whose path
  is the secret. Nothing the renderer strips afterwards can join two pieces
  into a key the redactor never saw. Reasoning arrives in
  500ms increments, so redaction and the </think> defence run over the
  joined buffer, where a key and its value are seen together, and a run that
  may still be the start of a key is held back until it has finished.
- A bubble near the server's ten-minute window is handed over to a fresh
  stream; a hand-over the server refuses stops rotation, and any stream the
  server has disowned takes no further frame — every closer falls back to a
  plain message instead of spending another refusal. Every frame a step
  writes, and both frames of a hand-over, take the reply write guard.
- The identity lookup is wired on the real boot path, and its absence logs
  a warning, since without it no bubble ever shows a step. A relay with no
  typing indicator attached warns once when progress frames arrive.

* fix(wecom): show a step the throttle held back without waiting for the next report

A step that landed inside the 1.5s refresh window was only shown when the
run reported again. The step most worth showing is often the last before a
long silence (a file read, then a test suite that runs for minutes), so the
bubble sat on the earlier step for the whole wait.

A held step now schedules one trailing refresh per feed for when the window
ends. It goes through the run's lane like any report, is not counted as a
sign of life for the rotation guard, is never published to other replicas,
and finds nothing once the round has ended.

Co-authored-by: multica-agent <[email protected]>

* fix(wecom): keep the reasoning buffer across a bubble hand-over

Redaction runs over the joined reasoning buffer so a label and its value
split across two increments are seen together. A rotation started that
buffer over, so `password=` at the end of the old bubble and `hunter2` at
the start of the new one were judged apart and the value went out in the
clear.

The new bubble now starts with no step lines and a fresh throttle but the
same raw reasoning buffer. The old bubble is sealed with the hand-over line,
so the reasoning is not shown twice.

Also gofmt the two-replica test.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-31 01:39:14 +08:00
0fe2ebf02e MUL-8045: feat(runtimes): Qoder Cloud Agent as a managed cloud runtime (#9402)
* feat(runtimes): add Qoder Cloud Agent managed remote execution

* fix(qoder): satisfy migration and localization CI contracts

* fix(qoder): bound execution and scope managed credentials

* fix(qoder): avoid upstream migration number collisions

* fix(qoder): gate issue creation and correlate tool results

* fix(qoder): synchronize guarded issue lifecycle transitions

* test(agent): publish Hermes holder identity atomically

* feat(agents): runtime-first create flow and cloud runtimes section for Qoder Cloud Agent

- Three-step create-agent flow (runtime -> configure -> done) with local,
  remote and cloud sections; Qoder connect form inline in the runtime step
- Runtimes page regrouped into remote and cloud sections with Qoder card
- Generalize Qoder storage to cloud_agent_connection (migrations 586/587)
- Locales for en, zh-Hans, ja, ko, fr; desktop and web routes for done step
- Fix status-picker fixture owner_id and diagnostics route coverage after main merge

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(runtimes): unify runtimes page into machines and cloud services

- One Add menu replaces Add remote runtime, the duplicated Add a computer
  and the web-only Cloud Runtime header button
- Group by machines / cloud services instead of local / remote / cloud, so
  web and desktop share one structure; this device is pinned with a badge
- Cloud services use the machine row anatomy in a single ListGrid table;
  unconnected Qoder is a row with Connect, coming-soon providers are rows
- Empty groups show the next step inline; search and online/offline filter
- Rename computer to machine across runtimes copy in all locales

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* test(runtimes): cover unified runtimes page; pin status and action tracks

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(runtimes): trim redundant descriptive copy on runtimes page

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(runtimes): keep row actions visible at narrow widths

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(runtimes): align Qoder add-menu item with other cloud services

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(runtimes): close the Qoder dialog after a first-time connection

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(agents): pick the runtime from one grouped list

The runtime step showed four kinds of cards, a comparison panel and
per-section descriptions. It is now one list in the runtimes-page layout:
machines (this one open, others folded to a row) and cloud services. An
unconnected Qoder opens the runtimes-page connect dialog in place and is
selected once it comes online.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agents): show owners as 'you' and bare version numbers in the runtime list

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(qoder): disconnect action and dedicated Qoder runtime page

Disconnect removes the encrypted connection and the managed runtime, unbinding
its agents after the admin confirms them. The Qoder row now opens its own page
(status, agents, usage, configuration, pause/resume, disconnect) instead of the
machine layout; generic runtime delete and visibility refuse a managed runtime.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agents): land on the done page after creating from step 2 on desktop

Desktop's location is the tab URL, which moves to /agents/new/done/<id>
while the configure page is still mounted. Its missing ?runtime= tripped
the skipped-step-1 redirect and replaced the tab back to /agents/new, so
Create looked like it did nothing. Only redirect on the manual route.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agents): show only settings a Qoder agent can run with

The Qoder bridge fails every task whose agent carries a model, thinking or
speed override, env, custom args, custom skills, MCP config or connected
apps, and it runs issues only. The agent detail page still offered all of
those editors, so one save could break every later run.

For a qoder_cloud agent the page now keeps Instructions under Capabilities,
General / Access / QCA Agent under Settings, drops the model rows and the
header model chip, shows the QCA agent in the summary, and locks the
runtime. Other agents no longer see the Qoder runtime in their picker,
since moving onto it needs a QCA agent the server would otherwise reject.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agents): hide conversation starters for Qoder agents

Starters only appear in a chat, and the Qoder bridge runs issues only.
The create flow already leaves them out for this runtime.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(runtimes): show the Qoder connection as cards with per-section editing

The Qoder runtime page rendered the whole connection as one always-editable
form. It now summarizes the connection, repositories and limits as read-only
cards; each section opens its own dialog and saves only that section.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(qoder): reuse a follow-up session only with the same repository and credential

Resolve repository credentials before deciding to continue a prior session,
and stamp each session with a fingerprint of its repositories, branches and
tokens. A follow-up continues only when the fingerprint still matches, so a
changed repository, branch or rotated/revoked token never rides into an old
session.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(qoder): keep a paused connection paused when its settings are saved

Saving repositories, limits or the connection no longer re-enables a paused
runtime. Resume goes through a new POST /qoder/resume.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agents): drop settings a Qoder agent cannot run when creating one

A draft started on another runtime could carry hidden skills, model
overrides and starters into a Qoder agent that then failed every task. The
create request now drops them for Qoder, and the API rejects skills and
execution overrides on Qoder agents at save time.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* chore(db): regenerate sqlc models for cloud_agent_connection

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(runtimes): never list the Qoder runtime under Machines

When the server could not run Qoder (no MULTICA_VCS_SECRET_KEY /
MULTICA_QODER_STATE_DIR), the managed Qoder runtime fell back into the
Machines group as if it were a computer.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(qoder): drop MULTICA_QODER_STATE_DIR

The managed bridge keeps its run journal encrypted in cloud_agent_connection
and a database advisory lock already elects one supervisor, so the state
directory only held a redundant file lock and acted as a feature switch.
Qoder is now available whenever MULTICA_VCS_SECRET_KEY is set. The
standalone qoder-runtime command still uses its on-disk state_dir.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(qoder): keep the branch in the session fingerprint after a journal restore

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(agents): validate Qoder agents' effective settings on every write

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: 安辰 <[email protected]>
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-22 01:33:38 +08:00
importcpp 67cec3fe3f fix(transcript): pair parallel tool results by call identity (#8567) 2026-09-19 18:13:17 +08:00
aac4aa3f44 MUL-7248: fix agent log duration timestamps (#8271)
* fix(transcript): preserve agent event timing

Co-authored-by: multica-agent <[email protected]>

* fix(transcript): keep timestamp fallbacks consistent

Co-authored-by: multica-agent <[email protected]>

* fix(migrations): resolve duplicate 458 prefix

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-10 22:18:16 +08:00
e04465be0b feat(transcript): tell the reader when a tool output was truncated MUL-7168 (#8212)
* MUL-7166 MUL-7167 feat(transcript): tell the reader when a tool output is incomplete

A tool_result preview is cut at 8 KiB and the remainder is never uploaded.
Nothing recorded that, so the run transcript rendered a truncated preview
exactly like a complete one — a reader checking what an agent did could take
the first 8 KiB of a 300 KB log for the whole thing.

The daemon now reports whether the preview dropped bytes, and the viewer says
so. Two separate claims, kept separate:

- Per step: a "truncated" badge next to the tool name when the record
  provably lost bytes.
- Per run, once: a caveat when the run contains output whose completeness
  nobody measured.

The flag is tri-state end to end (NULL/absent = unknown, false = complete,
true = truncated) and `output_truncated` is nullable with no default and no
backfill. Historical rows cannot be reconstructed, so they stay unknown; the
same applies to messages from an older installed daemon, which is why the
batch insert carries the value through the query's text[]/NULLIF encoding
rather than a []bool that cannot express the third state.

Display clipping is now worded as what it is. The inspector's 8000-character
clip said "... (truncated)", the same word the badge uses for bytes that are
gone, when copy still yields the whole stored text.

Verified: go test ./internal/handler ./internal/daemon ./internal/util
./internal/migrations, pnpm test (4981 views + core), typecheck, lint. Both
states captured in a local environment against real API + DB rows.

Co-authored-by: multica-agent <[email protected]>

* refactor(transcript): move the completeness note to the end of the output

The truncation indicator was a warning badge beside the tool name and a
run-level banner above the step list. Both announced the caveat before the
reader had a reason to care, and the badge's colour gave a routine 8 KiB cut
the weight of a failure.

"Was that all of it?" is a question you only have once you reach the bottom of
an output, so the answer goes there: one muted line at the end of the body,
in the same place the display-clip line already sits.

The unknown case follows the same rule rather than keeping its own banner —
the inspector shows one step at a time, so a note there appears once per
opened step, not once per turn in a list. That was the whole objection to the
per-turn disclaimer this design replaced; a run-level bar was one way to avoid
it, and putting the note where the evidence is another.

`outputCompleteness` replaces `hasUnknownOutputCompleteness`: the same rules
(tool results only, empty output excluded, false is a measurement) now answer
per item instead of per run.

Co-authored-by: multica-agent <[email protected]>

* fix(transcript): stop the display-clip line promising the whole output

"copy to get the whole record" reads as "the full output is in the database".
It is not: the daemon cuts at 8 KiB before reporting, and copy returns the
stored record — the same bytes, minus the 8000-character render clip. The
first reader of this string took it the other way, which is the whole reason
the truncation note exists.

Says what copy actually returns: everything that was saved.

Co-authored-by: multica-agent <[email protected]>

* fix(transcript): leave a stored tool result with one remark, not two

The render clip cut at 8000 characters, but a stored tool result is already
capped at 8192 bytes upstream — so on an ASCII output the clip could only ever
shave a further ~190 characters, and it did so under a note that already said
the rest was not saved. Two grey lines, the second reporting a rounding error
on the first.

The clip now sits above the source budget, where a tool result cannot reach it.
Tool input keeps it: input is persisted whole and has no server-side budget, so
it is the body that actually needs bounding — and it never carries a
completeness note, so that path stays a single remark too.

The marker also stops mentioning copy. It described the clip's reversibility,
which is only interesting next to a truncation note that is no longer beside it.

Co-authored-by: multica-agent <[email protected]>

* fix(transcript): say why the output is short, not where it stops

"Output ends here" reads two ways: the command produced this much, or we cut
it. Those are opposite facts about the agent's run, and the note exists to
settle exactly that question.

States the cause instead: the output was too long, so only the beginning was
kept. "Kept" rather than "shown" — the rest was never uploaded, and wording it
as a display limit invites the assumption that the whole output is in the
database and merely hidden.

The unknown note loses the same phrase for the same reason.

Co-authored-by: multica-agent <[email protected]>

* fix(transcript): address review on the completeness note

Three fixes from review of #8212, plus the frequency question it reopened.

A malformed `output_truncated` deleted the whole run from the screen. The
field parsed with `z.boolean().optional()`, so a string or a number failed its
row, the row failed the array, and `parseWithFallback` handed the viewer an
empty transcript. It now catches at the field, degrading to unknown. The old
test claimed this but passed a malformed top-level object, which exercised a
different path; the replacements fail without the catch.

The note appeared before the reader could have wanted it. `ToolDetailSurface`
collapses its own body, so a footer rendered outside it sat over a faded,
half-shown output — answering "was that all of it?" to someone still reading.
The surface now owns the note and withholds it until the body is revealed.

The render clip went back to 8000 characters for tool input. Raising it to
20000 was collateral: the goal was to stop re-clipping a tool result, which is
already capped at 8192 bytes upstream, and skipping the clip for `tool_result`
alone does that without touching how an unbounded input renders.

Unknown-ness moves back to the run. One daemon produces every message in a run,
so either it measured that run's outputs or it measured none of them — a fact
about the run, stated once, rather than a remark repeated on each step opened.

Co-authored-by: multica-agent <[email protected]>

* fix(transcript): drop the notice for records that predate the flag

A record from before the flag existed now says nothing. The viewer speaks only
when a daemon measured an output and found bytes missing.

The column stays tri-state. Nothing renders differently for NULL and false
today, but they are not the same fact, and defaulting NULL to false would write
"this daemon measured the output and stored all of it" onto rows where nobody
measured anything. The original lengths are gone, so that claim could never be
corrected — a lie the schema would keep telling long after this UI changed
again.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-10 14:40:45 +08:00
115bfab759 MUL-6523: persist task message batches in one statement (#7377)
* perf(server): persist task message batches in one statement

/api/daemon/tasks/{id}/messages is the hottest write path in the system:
every running task POSTs its buffered transcript every 500ms. The handler
was paying 3 + N database round trips per batch — GetAgentTask, the
workspace lookup inside requireDaemonTaskAccess, a SECOND identical
GetIssue/GetChatSession in the handler body, and then one
`INSERT ... RETURNING` per message, each in its own implicit transaction.

Two changes, both on that path:

- Take the workspace id from requireDaemonTaskAccessWithWorkspace, which
  already resolved it to authorize the daemon, instead of re-querying it.
  Broadcast reach is deliberately unchanged: the id is still only used for
  issue- and chat-backed tasks, so autopilot / quick-create tasks keep
  their current (non-)streaming behaviour.
- Add CreateTaskMessages, which expands the whole batch from one jsonb
  document via jsonb_to_recordset and inserts it with a single statement.
  A jsonb document rather than parallel arrays because `input` is itself
  jsonb and a text[]/uuid[] column cannot express a per-row NULL from Go;
  it also stays clear of the ~64k bind-parameter ceiling a multi-row
  VALUES list would hit.

Per batch this takes 3 + N round trips down to 3, and collapses N commits
into 1. Measured locally against Postgres 17 (200 iterations per shape,
same rows): 2.1ms -> 0.7ms at 3 messages, 7.0ms -> 0.8ms at 10, 80ms ->
1.1ms at 50.

Atomicity is the other half of the fix, not a side effect: the daemon does
not retry this endpoint, so a loop that failed halfway left a permanent
hole in the transcript. One statement makes a batch all-or-nothing.

The NUL sanitizer calls stay exactly where they were and matter more now —
jsonb rejects \u0000 for the whole document, so an unsanitized byte would
fail the entire batch rather than one row (GH #7098).

Co-authored-by: multica-agent <[email protected]>

* fix(server): guarantee task message publish order, test the real query

Review follow-ups on the batch insert.

Blocking:

- Realtime ordering had no contract. `INSERT ... RETURNING` defines no row
  order, but the publish loop walked the result as-is, so a batch could
  reach subscribers scrambled where the per-message loop always published
  in request order. CreateTaskMessages now returns through an ordered CTE
  (`WITH inserted AS (INSERT ... RETURNING *) SELECT * FROM inserted ORDER
  BY seq`), putting the ordering in the query rather than asking every
  client to re-sort. Verified it is not a no-op: with the ORDER BY removed,
  the new test reports the published order as [3 1 2] for a request listing
  seq 3, 1, 2 — i.e. the bug was real, not theoretical.

- The atomicity test copied the batch SQL by hand, so it could have passed
  while CreateTaskMessages was broken. It now calls the generated query.
  Both tests were also rewritten onto the repo's shared fixtures — dbfx for
  rows, testutil.JSONRequest / WithURLParams / Call(...).Want(...) for the
  handler — instead of open-coded INSERT/cleanup pairs and recorder
  quartets.

- New TestReportTaskMessagesPublishesInSeqOrder subscribes to the event bus
  and asserts the published seq order, with the request deliberately listing
  messages out of seq order so array order cannot pass for seq order.

Non-blocking:

- A failed `json.Marshal` of a message's input is no longer swallowed. A
  tool_use row that silently loses its arguments is worse than a 500 the
  daemon logs, because nothing anywhere records the loss.

- Corrected the atomicity claim in the query and test comments: one
  statement buys CONSISTENCY, not completeness. Without a retry a failed
  batch is now lost whole rather than leaving a prefix; actually closing the
  gap needs a retry plus a (task_id, seq) uniqueness rule, which is out of
  scope here.

The CTE makes sqlc name the result CreateTaskMessagesRow rather than reusing
the table model, so the publish site converts explicitly — a compiler-checked
cast that fails loudly if the query ever stops returning the whole row.

Co-authored-by: multica-agent <[email protected]>

* perf(server): pass task message batches as arrays, not a JSON envelope

Second review round. The jsonb-document parameter was a regression on large
messages: content and output had just been decoded out of the request, and
wrapping them back into JSON made the server escape every byte a second time
and Postgres parse the envelope out again. Measured against the per-row
insert it replaced, at one message: ~1.0x at 8KB, 1.46-1.48x at 128KB,
~3.0x at 1MB — worst exactly where the old path was cheapest, and those
messages are the least likely to be batched.

CreateTaskMessages now takes parallel arrays. NULLIF(value, '') is what makes
a per-row NULL expressible through the non-nullable []string sqlc gives a
text[] parameter, and it reproduces the single-row query's
pgtype.Text{Valid: x != ""} mapping exactly. input stays a text parameter
cast to jsonb in SQL — it is the one column that genuinely has to be parsed.

The arrays are zipped by several single-argument unnest calls in one SELECT
list, which expand in lockstep (PG10+ SRF semantics). The tidier
unnest(a, b, ...) form is equivalent but sqlc's analyzer only knows the
single-argument signature and fails to generate for it.

Measured after the change, same harness, two runs:

  one message, 8KB      0.98-0.90x of the per-row insert
  one message, 128KB    0.99-1.04x   (was 1.46-1.48x)
  one message, 1MB      1.59-1.67x   (was 2.99-3.02x)
  3 messages, 512B      2.7-3.2x faster
  10 messages, 512B     7.7-8.2x faster

128KB is now parity. 1MB is still above parity because any batch shape makes
Postgres materialize the array and unnest it before the insert, which a
direct single-row parameter skips; closing that needs a len == 1 fast path,
which review explicitly preferred not to have for the extra code path. A real
1MB single message is also unlikely: tool_result output is truncated to 8KB
in the daemon (internal/daemon/daemon.go). The durable answer for oversized
payloads is a body/batch cap on this endpoint, which is still a follow-up.

Also from review:

- The conflicting row in the atomicity test is built with dbfx.Insert instead
  of CreateTaskMessage, so test setup no longer shares code with the code
  under test.
- The ordering test runs on its own event bus and restores the shared one, so
  it stops leaving a listener behind on a bus that has no unsubscribe.
- The per-batch probe is Debug, not Info. At Info it was ~2 lines/second/task
  in production; it was added at Info in #7371 only to count batch sizes,
  which is answered.

Re-verified the ordering test still fails without the ORDER BY (reports
[3 1 2]) against the new query shape.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-21 15:17:02 +08:00
ea07a29138 perf(db): generate task message IDs as UUIDv7 (#7220)
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-19 17:17:28 +08:00
LinYushenandmultica-agent 5bacfd9742 MUL-2526 feat: add member(user_id, workspace_id) index + upgrade sqlc to v1.31.1 (#3046)
- Add migration 106: CREATE INDEX CONCURRENTLY on member(user_id, workspace_id)
- Rewrite ListWorkspaces to drive from member table with explicit fields
- Regenerate all sqlc code with v1.31.1 (intentional version upgrade)

Co-authored-by: multica-agent <[email protected]>
2026-05-22 12:26:56 +08:00
Jiayuan 3c93ebaf1c feat(agent): stream live agent output to issue detail page
When an agent is working on an issue, users can now see real-time output
in the issue detail page instead of waiting for completion.

Backend:
- Add task_message table and migration for persisting agent messages
- Add POST /api/daemon/tasks/{id}/messages endpoint for daemon to report
  structured messages (tool_use, tool_result, text, error) in batches
- Add GET /api/daemon/tasks/{id}/messages for catch-up after reconnect
- Add GET /api/issues/{id}/active-task to check for running tasks
- Broadcast task:message events via WebSocket
- Daemon forwards agent session messages with 500ms text throttling

Frontend:
- Add AgentLiveCard component showing live tool calls, text output,
  and progress indicators with auto-scroll
- Wire into issue detail timeline with WS subscription and HTTP catch-up
- Card appears when agent is working, disappears on completion/failure
2026-03-30 22:53:28 +08:00