mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
admin-workspace-create2
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
67cec3fe3f | fix(transcript): pair parallel tool results by call identity (#8567) | ||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
ea07a29138 |
perf(db): generate task message IDs as UUIDv7 (#7220)
Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
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]> |
||
|
|
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
|