mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 09:56:47 +08:00
admin-workspace-create2
63
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5acefb6dcc | fix(views): label text steps correctly in run details (#9153) | ||
|
|
13094caf7a |
MUL-7758: lay an issue's runs out in time (#8890)
* fix(issues): render run trigger snapshots as plain text (MUL-7758) A run's trigger snapshot is the comment's raw Markdown cut at ~200 runes, and the execution log printed it verbatim: an escaped ampersand read "&", and a comment that opens with a screenshot read "![CleanShot 2026-…" because the cut lands inside the image URL. Decode the entities in one pass, turn whole or cut images into a localized "[Image]" label, and keep links' text, so every surface that lists runs shows what the person actually wrote. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(issues): lay an issue's runs out in time (MUL-7758) The usage breakdown was a nine-column table of raw token counters headed by a token total that is 99% cache re-reads and a "cache savings" figure larger than the spend itself, with no way to tell when a run happened or to open it. The execution log showed tokens where the question is cost, and kept every past run behind a "Show past runs" toggle. Execution log (sidebar): - the header reads "21 runs · $166" and opens the new Runs dialog; narrow panels drop the count, never the cost's digits - a spend strip draws one bar per past run, oldest to newest, height = cost, with a baseline dot for runs that recorded no usage - agent time and elapsed time sit under it, active runs stay on top, the latest three past runs are listed with their cost, and "Open timeline" hands the rest to the dialog Runs dialog (replaces the usage breakdown): - spent / agent time / elapsed / runs, with failed and cancelled counts - a cumulative cost curve over per-agent run lanes on one time axis, the run that moved the total most labelled, hover summaries on every bar - runs grouped by day, newest first: who asked (quoted), which agent ran it, how long, a cost bar split by what was billed with the exact split on hover, and transcript / retry actions - runs without usage are listed with "—" instead of being left to a footnote, so the run count no longer disagrees with itself The timeline arithmetic lives in issue-run-timeline.ts with its own tests; retry moves to a shared RetryRunButton so both surfaces retry the same way. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(issues): open a run's transcript from its row (MUL-7758) A Runs row cut its trigger to one line and opened the transcript only from a small icon at the row's end, so a long ask could not be read anywhere and the obvious click did nothing. The whole row now opens that run's transcript; the trigger text is the row's real button for keyboard and screen readers, and retry stays its own action. The transcript dialog gains a "Trigger" line under its header that shows the ask whole and wrapped, for every surface that opens it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * revert(issues): open Runs rows' transcripts from the hover action again (MUL-7758) Row-wide clicks were a response to the transcript icon looking permanent; it only appears on hover, which reviewers found enough. Restore the per-row icon and retry actions. The trigger keeps its full-text native tooltip, and the transcript dialog keeps its Trigger line, so a truncated ask is still readable in full. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(issues): make the Runs chart answer the pointer anywhere (MUL-7758) Only the lane bars carried hover summaries, and most are a few pixels wide, so the chart read as static: hovering the curve did nothing. The plot column now snaps the pointer to the nearest run and shows a crosshair at its completion, the curve's reading there, and a card with the ask, agent, time, duration, the run's cost and the total so far; the other lane bars dim. Each run carries `costSoFar`, and `nearestRunIndex` does the snapping, both with helper tests. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): reach nested runs and open cost splits without hover (MUL-7758) Addresses the two review findings on the Runs dialog: - Overlapping runs: the chart snapped by time alone and broke distance ties by start order, so a run fully inside a longer one could never be picked, even with the pointer on its bar. The innermost bar now wins a tie, and a pointer over a lane only considers that agent's runs. - Cost split: the input / output / cache breakdown lived in a tooltip on a non-focusable span, out of reach for keyboard and touch. The cost cell is now a button that opens a popover on hover, click, tap or Enter. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): use radius tokens for the Runs chart marks (MUL-7758) CI's radius check rejects fixed arbitrary radii outside its allowlist. The lane bars, legend swatches and spend-strip bars used rounded-[2px]; they now use rounded-xs (3px), the same step the transcript's run timeline marks already sit on, rather than widening the allowlist. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5ed09342af |
MUL-7680: issue wakeup v2 — conditions, runaway protection, check-ins and visibility (#8807)
* feat(wakeups): add rule deadlines and expose the child-done system rule (MUL-7680)
Wakeups can now end: an absolute deadline (expires_at) or a relative wait
(expires_in_seconds) that restarts when the rule is re-enabled. When the
deadline passes first, the scheduler ends the rule; event rules with
on_timeout=wake run the target once with a wakeup.timeout fact. A timed-out
rule keeps disabled_at NULL so its timeout run stays claimable.
The implicit "wake the parent's assignee when a stage of sub-issues
finishes" behavior is now described by GET /api/issues/{id}/system-wakeups
and can be turned off or given a supplementary instruction per issue via
PUT /api/issues/{id}/system-wakeups/child_done. Rule reads fail open to the
previous behavior.
List responses add the creator (member or agent) and the new expiry fields.
The CLI gains --expires-in, --expires-at and --on-timeout.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(issues): let members create wakeups and show the child-done system rule (MUL-7680)
The issue sidebar's Wakeups section is always available on open issues and
gains a New wakeup popover. Members pick a condition (a time, a recurring
check with an end date, someone's reply, an agent's run ending, or raw
events), the agent to wake, the instruction, how often it fires, how long
to wait and what happens on timeout. It posts the same configuration agents
create with the CLI.
The parent's child-done wake appears as a System rule with its stage,
remaining sub-issues and target, a per-issue toggle and a supplementary
instruction. Rows show when a rule ends, timed-out rules read as such, and
details name who created the rule. Rule titles wrap to two lines instead of
truncating.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* docs(wakeups): document deadlines, member-created rules and the system rule (MUL-7680)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(wakeups): platform conditions, runaway protection and check-ins (MUL-7680)
Conditions: a wakeup can now wait for a fact the platform checks itself
(a status, assignee, label or property value; sub-issues or one stage
finishing; a linked PR's checks finishing or merging; another issue's
status). Related events only prompt an early evaluation; the rule wakes
its agent when the predicate becomes true, and a repeating rule fires
again only after it turned false or its facts changed. Finished PR checks
from before registration are ignored.
Runaway protection: repeating event rules stop after max_fires runs
(default 20), a rule pauses when its causal chain passes through it a
third time without a person in between, or when it starts 12 runs in an
hour. The reason is stored and shown.
Also: silent check-ins for every/cron checks (the run then posts no
fallback comment), wake now, delete, a per-rule run history, paused-rule
lists, timeline entries for created/triggered/timed-out/paused/check-in,
and source, paused scope, 7-day runs and child-done system rows in the
workspace list. The CLI gains --until-* conditions, --max-fires and the
trigger/delete/checkin/runs commands; the brief and wakeup prompt state
the check-in exception.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(issues): record child-done transitions in the status write (MUL-7680)
The child-done system rule ran after the status write committed and gave
up on any error, so a crash, deploy or transient failure lost the
parent's wake. A trigger now records each child's move into a closed
status in the writing transaction, for every writer. The request that
wrote it processes the rows right away and a scheduler sweep retries
anything left over; claims make the two paths process a transition
once.
The rule also follows a workspace default (settings key
system_wakeup_child_done) until an issue sets its own, the per-issue
update accepts partial bodies, and each trigger adds a timeline entry
that names its system comment.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): wakeup conditions, history, timeline and workspace list (MUL-7680)
Issue sidebar: members can create the platform-checked conditions (a field
reaching a value, sub-issues or a stage finishing, a linked PR's CI or merge,
another issue's status) and cap how often a repeating wait fires. Rule
details show the fire count, why the platform paused a rule, the latest runs
with silent-check notes, and offer wake now and delete.
The issue header says what the issue is waiting for ("Emacs 在等 Jiayuan 回复
+2") and opens the Wakeups section. Board and list cards use the same short
sentence, or flag a paused rule. Wakeup runs read as "由唤醒触发 · <condition>"
in the execution log, transcript and on the comments they post.
The timeline shows created, triggered, timed-out, paused and check-in entries
with the rule they came from; consecutive check-ins merge, and the child-done
entry stands in for its system comment, which it can reveal.
Automation → Issue wakeups adds a source column and filter, 7-day runs, a
paused scope with a banner, the child-done system rule on each waiting parent,
and "New wakeup" with an issue picker. Settings → Issue statuses sets the
system rule's workspace default. Copy in all five languages.
The server records the watched issue's identifier in other-issue conditions
and the rule's creator in timeline snapshots, and returns board summaries with
their condition.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* docs(wakeups): document conditions, runaway protection and check-ins (MUL-7680)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(wakeups): list system rules without a revision and localize statuses (MUL-7680)
System rule rows returned revision 0, which clients reject as a rule
revision; they now return null, and a list row with an invalid revision no
longer fails the whole page. Built-in statuses in conditions and the status
picker read in the viewer's language. Adds a browser test for a condition
created from the form, the header summary, the scheduler waking the agent
and the workspace list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): say why a rule paused without repeating "paused" (MUL-7680)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(i18n): use the product's task terms in wakeup copy (MUL-7680)
ja, ko and fr wakeup strings said イシュー / 이슈 / ticket; the product term
for an issue in those languages is タスク / 태스크 / tâche (and sub-tasks
accordingly), per the conventions page. French strings are rewritten for
the feminine noun.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* test(views): pin the shortcut platform in the wakeup form test (MUL-7680)
The send shortcut is primary+Enter, which is Ctrl on Linux CI runners, so
pressing Cmd+Enter only submitted on macOS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): simplify the workspace wakeup list
Put the filters on one row: scope as a segmented control on the left,
source/trigger/agent and a search-as-you-type box on the right. "New
wakeup" moves to the page header, which drops the separate Search button.
Rows are single-line: the identifier sits before the title, frequency and
timezone move into the trigger tooltip, and selection, prompt edit and
the last run's transcript show on row hover or focus. The issue column
takes the remaining width and truncates, so the table fits the page.
Also fix creating from the list: the issue picker closes itself right
after reporting the selection, and that close cancelled the flow before
the form could open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* refactor(server): run the child_done rule as a system issue wakeup
The parent-assignee wake on closed sub-issue stages was its own code path:
a system comment, a mention-style run, an override table and endpoints.
It is now an ordinary issue_wakeup row with system_rule set, sharing the
condition, receipts, runaway protection, timeline entries and run model of
people's rules.
- One children_done evaluator: a stage wakes the assignee when it closes
while a later stage waits; the wrap-up waits for every sub-issue,
unstaged ones included. People's sub-issue conditions read the same set.
- issue_child_event records closing, reopening, re-parenting and restaging
for every writer; the request processes it right after commit and the
sweep retries. User sub-issue rules become immediate too.
- The target is resolved when it fires: an agent run, a squad leader run,
an inbox notification for a member, or only the timeline entry.
- A parked (backlog) parent catches up when it leaves backlog; a waiting
run of the same agent is joined instead of queuing a second one; the
hourly limit pauses a runaway rule. No system comment is posted.
- The run gets the issue's instruction, else the workspace's, else the
built-in one, plus each stage's counts and the next stage.
- Workspace defaults move to GET/PUT /api/system-wakeups (owners and
admins) and apply to every rule nobody customized; turning a rule on
records what already holds so old facts do not fire.
- Existing open parents get their rule from a one-time backfill.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): workspace wakeup settings and sub-issue notifications
- Settings → Wakeups holds the sub-issue rule's workspace default and
default instruction; the toggle leaves Issue statuses.
- The issue's system rule shows who it reaches (a member is notified),
a platform pause, and the instruction it will use.
- Timeline entries say whether the assignee was woken, notified or joined
a waiting run. The folding of the old system comment is gone.
- Inbox renders the new children_done notification on web, desktop and
mobile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(server): stop wakeups from repeating their agent's runs
Runs are serialized per issue and agent, so a wakeup never ran beside
another run of its agent, but it queued behind one and repeated it. Each
rule now checks two things before it starts a run:
- Acknowledged: every input came from the agent itself. Its own comments
and issue changes never wake it, and a condition its own unfinished run
on the issue satisfied does not wake it when the platform or the agent
set the rule up. This fixes #8849: a coordinator closing its own stage is
not woken again while that run is going. A person's condition rule still
runs afterwards, since the running agent lacks its instruction.
- Merged: when a run of the agent is already waiting to start on the issue
(assignment, comment, another rule), the rule's instruction and facts
join that run (context.wakeup_joined, sent to the daemon as
wakeup_joined and rendered for every prompt kind) instead of queuing
another. Turning the rule off or replacing it withdraws what it joined.
Sub-issue changes now record the run that made them, and hints and
condition.met inputs carry those runs so a satisfied condition knows
whether the agent caused it. A rule that names the agent itself as the
actor keeps waking it on its own changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): show merged and acknowledged wakeups, hint assignee comment rules
- Timeline entries say when a wakeup joined the agent's waiting run or was
the agent's own doing, for people's rules and the sub-issue rule.
- The create form tells a member that a reply or comment rule for the
issue's agent assignee joins the run a comment already starts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(server): hand waiting wakeups to a run when it is claimed
A rule that fired while its agent had a run waiting used to write its
instruction into that run right away and consume its inputs. That let a
member's rule run under another member's identity, kept deleted rules'
instructions in the run, lost the inputs when the run was cancelled or
claimed by an older daemon, and skipped the fire cap and loop check.
Now the rule only waits for a run that runs as the same person its own
run would, keeping its inputs. When a daemon advertising
joined-wakeups-v1 claims that run, JoinWaitingWakeups rechecks each rule
(on, same agent and person, creator still allowed, not the agent's own
input, no loop), consumes its inputs, counts the fire toward max_fires,
adds its chain and records the merge. A re-claim drops entries of rules
turned off or changed since. Anything else leaves the rule its inputs to
start its own run.
A child_done rule created by the backfill or by processing a change now
treats sub-issues with unprocessed close events as still open, so the
backfill no longer swallows a close waiting for the sweep.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(server): consume joined wakeup inputs only once the run starts
A claim used to consume the inputs of the rules that joined the run. If
that claim did not go through, the run went back to the queue carrying
inputs the rules no longer had: turning the sub-issue rule off on the
issue or for the workspace (which only changes enabled) did not remove
them from the next claim, and cancelling the run lost a once rule that
had already ended.
A claim now reserves the inputs for the run (receipt task_id, still
unprocessed). The rule's next dispatch settles them: a run that started
consumes them as a merged firing (once ends, max_fires counts and can
pause, timeline entry); a run that ended without starting gives them
back; one that has not started keeps them. Turning a rule off or
changing it discards its pending inputs, reserved ones included, and a
later claim of the same run rechecks every entry and drops the ones
without reserved inputs or no longer allowed to reach the run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
1d94166dc2 |
MUL-7642: full-window attachment viewer that pages through every file (#8763)
* feat(attachments): full-window viewer that pages through every file (MUL-7642) The preview was a centered card capped at max-w-6xl, so a screenshot never showed larger than 1152px wide whatever the screen, and prev / next only walked images. - The viewer takes the whole window: no card, a near-black stage, a top bar in the dark token set, prev / next in 64px side gutters, type / pixel size / file size instead of the MIME type. Markdown and text read on a centered sheet in the app theme; PDF and HTML fill the stage. - The surface sequence now covers every previewable kind, so an issue pages through its screenshots, PDFs, reports and HTML in render order. `collectAttachmentSequence` takes the inclusion rule; mobile keeps the images-only `collectImageSequence`. - The issue description block no longer lists every issue attachment as its own: comment uploads keep issue_id, so they were counted at the description's position. - A focused video / field keeps its arrow keys. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(attachments): viewer image swap and desktop window chrome (MUL-7642) Review of #8763: - Paging from a PDF (or any non-image) onto an image that had not decoded yet handed the PDF's URL to <img>; its load error was blamed on the image being opened, which got marked broken and skipped. The settled-URL hook now only ever holds a URL that decoded as an image, and the <img> reports errors only when it shows the current file. - On desktop the full-window top bar sat under the macOS traffic lights and over the app's top-bar drag region. The viewer now enters immersive mode while open, marks itself no-drag, lets its top bar drag the window, and opts the bar's controls back out. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(attachments): viewer waits for the media re-sign before loading (MUL-7642) On clients that cannot load `/api/attachments/{id}/download` natively (desktop, split-origin web, self-host proxy mode) the viewer handed that endpoint to <img> while the re-sign / authenticated byte fetch was still in flight. The native load failed, and in a sequence the failure read as a broken image: it was marked unavailable and skipped before the loadable URL arrived. `useResignedInlineMedia` now also reports whether the upgrade is pending. The viewer holds the previous frame (or shows loading) until it settles, and the neighbour prefetch waits too. The inline figure keeps its behaviour. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(attachments): no focus ring around the viewer stage on open (MUL-7642) The viewer focuses its zoom canvas on open so +/-/0 and the arrows work without a click. That programmatic focus can match :focus-visible, and on the full-window viewer the canvas ring drew a light frame around the whole stage. The canvas now marks focus it placed itself; the stylesheet drops the ring for it until focus leaves, so tabbing back in still shows one. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
0516dd57c3 |
MUL-7625: feat(antigravity): stream live tool execution events (#8734)
* feat(antigravity): stream live tool execution events * chore(i18n): remove obsolete French Antigravity live notice * fix(antigravity): preserve tool events under backpressure * fix(antigravity): retain tool errors in bounded previews * fix(antigravity): normalize tool parameters for transcript UI * fix(antigravity): move tool aliases and normalize file content |
||
|
|
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]> |
||
|
|
378e3bd24b |
MUL-7154: unify the global radius system (#8175)
* fix(ui): unify global radius tokens Co-authored-by: multica-agent <[email protected]> * fix(ui): reduce global radius scale Co-authored-by: multica-agent <[email protected]> * fix(ui): address radius review findings Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5132ac99eb |
MUL-7235: show cancellation actor in run history (#8249)
* MUL-7235: show cancellation actor in run history
Co-authored-by: multica-agent <[email protected]>
* fix(issues): simplify failed run recovery UI
Co-authored-by: multica-agent <[email protected]>
* Revert "fix(issues): simplify failed run recovery UI"
This reverts commit
|
||
|
|
a60713570a |
MUL-7227 perf(transcript): reuse timeline work for messages that have not changed (#8242)
* perf(issues): stop inline runs re-scanning the whole transcript (MUL-7227) #8131 moved the agent transcript out of a click-to-open dialog and into the comment thread, where a live run's timeline is rebuilt on every 100ms `task:message` flush window. Rebuilding means redacting, and redacting means running 13 patterns over every byte of every message body — so a run with a 3000-message transcript cost ~25ms per flush, ~250ms of main thread per second, per live run. Two or three agents streaming on one issue is a beachball. Nothing the inline run renders needed that. Every summary it draws is redacted where it becomes a string, and `StepBody` redacts every body it puts on screen; only the full-log dialog renders `item.content` directly. So `buildTimeline` splits into `buildTimelineStructure` (coalesce, ~0.1ms at any size) and the `redactTimelineItems` pass, and the inline run derives structure while the dialog — mounted only while open — gets the redacted copy. Measured on the real chain through `buildSteps`/`groupSteps`: 24.6ms → 0.2ms per flush at 3000 messages. `buildTimeline` keeps its meaning, so every other caller is unchanged. Also drops `refetchOnWindowFocus: "always"` from `useTaskMessages`. That overrode the app-wide default and defeated `staleTime: Infinity` on an endpoint that returns the whole transcript with unbounded tool payloads and does not paginate, so every alt-tab back into the desktop app re-downloaded and re-merged every transcript on screen. Frames keep the cache current while the window is in the background, and the existing effect already backfills the two transitions focus was covering — a run becoming visible, and a live run reaching its terminal state. Not addressed here, and worth its own change: mounting `useTaskMessages` for a run registers the query cache entry even when `enabled` is false, and entry presence is what `isTaskMessageTimelineHeld` gates `task:message` caching on (MUL-6396). The held set therefore widened from "the open transcript" to "every run rendered on the issue". That is now cheap to hold, but it is still a wider gate than the one #7227 designed. Co-authored-by: multica-agent <[email protected]> * fix(transcript): redact bodies before anything clips them (MUL-7227) Review of #8242 found that deriving the inline timeline unredacted put raw bodies in front of clips that redact afterwards, and a pattern only matches while both its ends are present. `StepBody` cut at its display clip and then redacted; a step summary cut to 200 characters and then redacted. A secret straddling either cut lost the tail its pattern needed and rendered its head. The whole-transcript redaction this branch removed had been hiding that ordering, in the dialog and everywhere else. Redaction now happens where it did before — over a whole body, before any cut — but on the bodies being rendered instead of on every message in the transcript. `redactTraceRow` / `redactTraceStep` redact the rows on screen and the one step the collapsed summary reads; `StepBody` redacts, then clips. The bound is what this branch is for: a live run rebuilding on each 100ms flush pays for a dozen rows, not for thousands of messages. Measured on the same chain as before: ~15-25ms per flush at 3000 messages, now ~0.2ms. Also closes a leak the review turned up while auditing that path, older than this branch: `DiffDetailSurface` handed raw lines to the syntax highlighter and rendered its markup with `dangerouslySetInnerHTML`, so `redactSecrets` only ever ran on the fallback — redaction was conditional on a language lookup failing. Diff lines are built from tool input too, which carries no upstream redaction at all, so an `Edit` to a recognized file type displayed whatever the agent wrote. Lines are redacted once, before highlighting. Second finding: removing `refetchOnWindowFocus` left the initial handshake uncovered. The transcript fetch and the WS subscription are not ordered, so a row persisted between them is in neither the response nor any frame this client received, and `staleTime: Infinity` means nothing fetches it again — the run stays live and visible, so no mount or liveness transition fires either. `onReconnect` deliberately skips the first connection, which is exactly the one that matters here. `WSClient.onConnect` fires on every authenticated connection, and the sync layer uses it to invalidate `task-messages` alone — once per connection rather than on every window focus. Six regression tests, each verified to fail without its fix. The redaction ones render the real `StepBody` and detail surfaces; the inline suite mocks them, which is why these boundaries were missed. Verified: pnpm typecheck, pnpm lint (0 errors), pnpm test — core 1724, views 4995, desktop 626, web 266, docs 15. Reported-by: Niko Co-authored-by: multica-agent <[email protected]> * fix(transcript): redact tool arguments, and make the connect repair a barrier (MUL-7227) Three findings from review of |
||
|
|
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]> |
||
|
|
5be9f3b18c |
MUL-7151 fix(issues): stop the full-log trigger from keeping focus after a pointer open (#8169)
Closing the run log with Esc handed focus back to the trigger. Because Esc is keyboard input, that restored focus matched :focus-visible even for a reader who opened the log with a mouse — the trigger came back ringed and tooltipped, and the hover-revealed comment action row stayed on screen. Return focus only when the log was opened from the keyboard (a click with no detail count, the signal the run disclosure already uses). Where the trigger and the dialog live on different TranscriptButton instances — the active-work popover row vs the header chip — the open modality travels with the open signal so the dialog-owning instance still restores a keyboard reader's focus. Covered by two real-dialog focus suites: inline-comment-run.focus.test.tsx and issue-agent-header-chip.focus.test.tsx. |
||
|
|
028f3b4437 |
feat(issues): show agent execution inline in comment threads (#8131)
* feat(issues): show agent runs inline with comment replies * fix(issues): unify run comment projection and preserve disclosure focus * refactor(issues): replace nested run cards with inline status rows * fix(issues): align thread roots with reply content * style(issues): remove thread toggle background * style(issues): group reaction picker with comment actions * feat(issues): show readable issue references in run summaries * style(issues): move completed run logs into comment headers * style(issues): balance comment block vertical spacing * style(issues): shimmer live agent progress text * style(issues): unify comment action hover feedback * style(issues): reveal comment actions on block hover * fix(issues): move published reply activity into the header * fix(issues): settle comment UI as soon as replies are published * fix(issues): reveal comment log buttons on hover * fix(issues): defer run placement until trigger comments arrive * fix(issues): isolate agent comment queues by thread * fix(issues): display pending runs in execution order * fix(ci): align integration tests with thread-scoped queues * fix(issues): fit run actions to narrow comment columns |
||
|
|
54e641aaa3 |
MUL-6665: rename agent task records to runs (#7534)
* fix(copy): rename agent task records to runs Co-authored-by: multica-agent <[email protected]> * fix(copy): finish run terminology migration Co-authored-by: multica-agent <[email protected]> * fix(copy): address terminology review follow-ups Co-authored-by: multica-agent <[email protected]> * fix(docs): address final run terminology review Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: Lambda <[email protected]> |
||
|
|
d276991c60 |
MUL-6743 refactor(issues): remove the assignment handoff note (#7626)
* refactor(issues): remove the assignment handoff note (MUL-6743) Usage data puts the handoff note at 0.2% of assignments, so the box occupied the confirm dialog's centre for the other 99.8% and bought them nothing. Removing it takes the whole feature with it: the note input and its runtime version gate, the `handoff_note` write field on PATCH/batch issue updates, the task column write, and the daemon's opening-prompt + issue_context.md handoff sections. The confirm dialog itself stays — it still names who is about to run and still offers "Don't start yet" (suppress_run). The `*WithHandoff` enqueue methods only ever varied by that note, so they become `EnqueueTaskForIssueByActor` / `EnqueueTaskForSquadLeaderByActor` — the actor-carrying variants autopilot already called with an empty note. `preview-trigger` drops `handoff_supported`, which nothing read after the gate moved client-side. `agent_task_queue.handoff_note` is deliberately left in place: it holds historical rows and this is the expand half of an expand-contract pair. Nothing reads or writes it now, so a follow-up migration can drop it once no deployed binary selects it. Co-authored-by: multica-agent <[email protected]> * docs(issues): reshoot the assignment confirm dialog (MUL-6743) The asset still showed the handoff note box the surrounding prose no longer describes. Recaptured from the app at the same 600x450 frame; the confirm button now carries the send-chord keycaps it has shipped with since MUL-5694. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
603519d158 |
MUL-6977: test: remove implementation-only coverage (#7932)
* test: remove implementation-only coverage Co-authored-by: multica-agent <[email protected]> * test: remove remaining implementation-only coverage Co-authored-by: multica-agent <[email protected]> * test: restore compatibility and fragment navigation coverage Co-authored-by: multica-agent <[email protected]> * test: preserve behavioral cleanup contracts Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
316b895959 | fix(i18n): respect locale for formatted values (#7757) | ||
|
|
11861145ab |
MUL-6758 feat(core): add truncateWithEllipsis string helper (#7646)
Adds `truncateWithEllipsis(text, maxLength)` to packages/core/utils.ts and routes two existing truncation sites through it. The helper appends the single-character ellipsis `…` (U+2026), counts it toward the budget, and measures that budget in Unicode code points rather than UTF-16 code units, so an emoji is never cut into a lone surrogate. Trailing whitespace is trimmed before the ellipsis is appended. Adopted by the task-transcript trace presenter and the desktop daemon panel. Both previously used a content-character budget, so each passes `max + 1`. Content of length exactly `max + 1` is now shown in full rather than truncated — the old code spent an ellipsis to hide a single character. Closes #7544. Co-authored-by: Danila Katalshov <[email protected]> |
||
|
|
07bc8bc6bc |
MUL-6739 fix(chat): keep the viewport at the live end while replies stream (#7612)
* fix(chat): keep the chat viewport at the live end while a reply streams (TIM-55) Virtuoso's `followOutput` only fires when the item COUNT changes. A streaming assistant turn is one row that keeps growing, so nothing followed it and the reply ran off the bottom of the fold. Virtuoso's own SIZE_INCREASED recovery is armed for 100ms after a count change, which a multi-minute stream leaves behind on its first token. The composer is the same failure from the other side: it grows with the draft (and banners/queue appear above it), shrinking the list's viewport while scrollTop stays put, so the tail slides out of view behind it. The list now drives its own bottom-stick off a ResizeObserver watching both the scroll container and the content inside it: while the reader is at the live end, any content growth or viewport shrink re-pins to the bottom; scrolling away releases the pin until they come back. `followOutput` drops to "auto" — the stick pins instantly, so a smooth glide started on a count change was only going to be cancelled by the next chunk. * refactor(chat): collapse the bottom-stick controller into pure helpers The stateful BottomStick factory carried one bit of state (pinned), which now lives in the hook's ref. The decisions stay as pure functions (isAtLiveEnd, bottomPinTarget) so the node-side matrix tests survive, and HTMLElement satisfies ScrollMetrics structurally, removing the readMetrics adapter. Pin-state transitions (release, re-engagement) are now hook behavior and move to the DOM suite. * refactor(chat): trim auto-scroll comments * fix(chat): preserve scroll-away intent during streaming * fix(chat): preserve pin through live-end clamps * fix(chat): judge scroll-away intent from input via the shared live-end latch Review feedback on #7612: releasing the follow on any 1px upward move regressed the base's 120px forgiveness and left no recovery path, and the repo already ships a latch that solves reader-intent-vs-system-displacement for the newest-first transcript. The latch's state machine is direction agnostic, so the chat list now reuses it: generalized to createLiveEndFollow (fed away-positive input deltas and distance-from-bottom), wired to wheel/touch/key/pointer input mirroring the transcript dialog. Sub-threshold nudges keep following, accumulated input past FOLLOW_EDGE_THRESHOLD releases, clamps and streaming never count as intent — no direction sensing, no 1px special case, one shared threshold both directions. Also from review: content growth now arrives through Virtuoso's own totalListHeightChanged instead of a wrapper div, the #6697 auto-vs-smooth rationale is restored, and followOutput has direct coverage for a released follow while Virtuoso still reports atBottom. * docs(chat): make auto-scroll comments standalone * fix(chat): stage scroll input until the surface's own scroll confirms it Review feedback on #7612: the latch credited every wheel/touch/key event that reached the container, including input the list never consumed (a nested code-block scroller, a conversation too short to scroll) — releasing the follow with no scroll event to ever re-engage it. The keydown target guard had the mirror problem: the container is not focusable, so paging from a focused row control scrolled the list with no input recorded, and the reader was pinned back over their own keypress. input() now stages intent and only the surface's own scroll promotes it past the threshold; resize-driven displacement (content growth, composer changes) can never promote, and unconsumed intent expires with its gesture. That lets the chat keydown wiring drop the target guard entirely: a key a control consumed never scrolls the list, so it never releases, while one the browser answered does. Home/End now stage intent like the other keys instead of disengaging unconditionally. * fix(chat): let the surface's scroll supply the release magnitude Review feedback on #7612: releasing on input magnitude inside one 300ms burst made release unreachable at reading pace (discrete wheel notches reset the accumulator every gesture, and a Firefox notch is exactly the threshold), and gating the pin on input freshness let a reply's final chunk land inside the window and park unpinned forever. Input now only opens a per-gesture budget; each scroll attributes the displacement the surface actually took, capped by that budget, so a system shift inside a gesture (a transcript prepend) can never count as reader intent. Attributed displacement accumulates until the reader returns to the live end — it survives both gesture boundaries (reading-pace notches release) and pins (persistent sub-threshold attempts against a fast stream eventually win). The pin verdict no longer consults the intent window at all, so displacement is corrected immediately with no timer for a final event to hide behind. Also from review: Shift+Space pages up, so it now stages away-intent in the chat list and toward-intent in the transcript dialog, where the unsigned handling predated this PR. * fix(chat): attribute confirmed touch momentum * test(chat): cover active touch pin guard * fix(chat): cap confirmed non-touch motion by its unspent claim Confirming a scroll opened a motion window that any same-direction displacement could extend for 300ms, with no source or magnitude attached. A transcript prepend landing in that window was credited to the reader: 30px of real movement, still inside the edge zone, released the follow permanently — the #5921 case the latch exists to prevent. Touch keeps coasting freely, since momentum is not proportional to finger travel. Every other source may now only continue for the claim the surface has not spent yet, so a wheel notch whose scroll animates across frames still counts while a system shift finds nothing to draw on. Co-authored-by: multica-agent <[email protected]> * fix(chat): make carry survive fractional frames and overlapping notches Two ordinary wheel sequences still reached the pin. An exact `moved <= motionCarry` rejected the last frame of an unevenly split animation by about one ulp, because carry is spent in fractional steps and drifts from the surface's own arithmetic. The comparison now allows a tolerance scaled to the amount at hand; a real system shift is pixels, not ulps. A notch arriving while the previous one was still animating was never confirmed: the continuation branch spent the old carry and returned, so endInputFrame() discarded the new claim and the rest of the reader's own scroll was pinned back. Same-direction scroll now folds a fresh claim into the carry first, on both the away and toward paths. Co-authored-by: multica-agent <[email protected]> * test(chat): make the toward-fold regression actually exercise the branch The case started by scrolling 300px away, which released the follow — so the pin verdict was false no matter what the toward fold did. Park the viewport out at 300px under a held pointer instead, which suppresses the pin without attributing anything, and the reader is still following when the notches land. Removing the fold now fails this test. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
7b0b239779 |
MUL-6551 fix(i18n): separate task failure labels from raw diagnostics (#7496)
#7459 localized the `failure_reason` labels, but the raw `task.error` the server and daemon persist was still concatenated into what the user reads. A non-English workspace saw a translated reason followed by an English sentence, and hover text carried absolute worktree paths and machine names. Draw the line by audience instead: - The localized reason is the entire user-facing explanation. Drop the raw `task.error` from the execution log's status tooltip, the agent activity row's tooltip, and the transcript's cancelled badge. - In the transcript's Run details, "Reason" now heads the localized label from the stable `failure_reason` enum, and the raw text moves to its own "Technical details" row at the end — a translated heading over untranslated content, still readable for whoever debugs the run. Also finish the two label tables #7459 left inside the translated package: - `taskStatusConfig.label` was dead (the sole consumer reads only icon and colour; the status word already comes from `taskStatusLabel`). Removed. - `availabilityConfig` / `workloadConfig` kept English `label` strings whose only reader was `AgentStatusDot`'s `aria-label="Status: Offline"` — the whole accessible payload for a screen reader, in English in every locale. It now reads through the existing `availability.*` keys plus a new `presence_status_aria`, and the visual configs carry no copy at all. New keys in all four shipped locales (en, zh-Hans, ja, ko): `presence_status_aria`, `transcript.details_diagnostics`. Tests: the visual configs are asserted to expose no `label` field, the headings are asserted distinct per locale, the raw error is asserted absent from tooltips while still present in Run details, and the presence dot's accessible name is asserted in Chinese across every availability value. No schema, API, or server change — `agent_task_queue.error` keeps its exact current meaning, which is what `taskfailure.Classify` matches on. Refs #7411 Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9ecc6787b0 |
fix(i18n): localize task failure reasons [MUL-6565] (#7459)
* fix(i18n): localize task failure reasons * fix(i18n): cover operational failure reasons * fix(i18n): cover legacy user cancellation reason |
||
|
|
e5b28564b0 |
fix(views): prevent crowded transcript end ticks (#7458)
Co-authored-by: multica-agent <[email protected]> |
||
|
|
7fdc854c26 |
fix(issues): copy durable local path for worktree tasks (MUL-6378) (#7176)
A worktree-mode local_directory task runs in a disposable git worktree that Finalize removes on every exit path, so the task's `work_dir` pointed at a directory that no longer existed. "Copy local workdir path" handed the user a path they could not `cd` into. The daemon now reports a `durable_work_dir` handoff instead of having clients infer one: - `removeLocalWorktreeDir` returns an error and confirms removal with `Lstat`, so a broken symlink cannot be mistaken for an absent directory. Finalize fails when removal cannot be confirmed. - `durable_work_dir` is written only after Finalize succeeds and the worktree is verifiably gone. A preserved worktree, an in-place run, a running task, a squad-leader task, and any older daemon all leave it unset, keeping the real `work_dir` authoritative. - The field travels on the complete, fail, and cancel-ack callbacks and is persisted on the task row (migration 362, additive nullable column). Writes use COALESCE for replay idempotency; the cancel-ack path CASes on `status = 'cancelled'` so a late ack cannot touch a completed or failed row. - The server derives `relative_durable_work_dir` through the same home-stripping helper as `relative_work_dir`; the absolute value is reserved for explicit clipboard actions and is never rendered. - Clients select the durable path only for a terminal task that carries the field. The issue actions menu and the transcript dialog share one resolver, and the label and toast say the task worktree was reclaimed, naming the delivery branch when one exists. The stored path is a point-in-time delivery snapshot: later renames or detachments of the project resource do not rewrite historical tasks. |
||
|
|
0c0baf0505 |
perf(realtime): stop the task:message fanout from flooding every client (MUL-6396) (#7227)
* perf(realtime): stop the task:message fanout from flooding every client (MUL-6396) `task:message` is broadcast to every client in the workspace for every run, and the client cached all of it: the handler's `(old = [])` default built a timeline entry on first sight, so each client accumulated the full transcript of runs its user would never open, held for the life of the run. Tool input is unbounded on the wire, so a single large Write shipped its whole body to every open client and was retained there. Three changes, each useful on its own: - Client drops frames for tasks no mounted view is rendering. Presence in the query cache is the signal, which registers when a view mounts and before its fetch resolves, so a frame landing mid-backfill is still kept. Rows are persisted before they are broadcast, so a dropped frame costs nothing — whoever opens the task next fetches it. - Server clips oversized `input` / `output` out of the broadcast copy and marks it `truncated`. Persisted rows and the REST list responses are untouched. A client seeing the flag repairs that task's timeline from the DB, once per flush window rather than once per frame. - Client coalesces surviving frames into one cache write per 100ms window, turning a burst of streamed messages into one merge and one render. The window is fixed, not a resetting debounce, so a continuous stream still flushes instead of being starved until it pauses. Mobile keeps its own per-session updater and was already scoped to mounted sessions, but shares the wire format, so it refetches on `truncated` too. The remaining half of the batching idea — one WS frame per reported batch instead of one per message — is deliberately not here. The client identifies itself on the upgrade URL but the server does not retain that version, so a shape change would silently break every older client. It belongs with the client scope-subscription work that `listeners.go` is already waiting on. Co-authored-by: multica-agent <[email protected]> * fix(realtime): keep broadcast clipping off the wire, and stop a late fetch dropping live seqs Addresses both must-fix findings from review. 1. Clipping the broadcast copy is not something an existing client can ignore: it changes what `input` / `output` mean, and every deployed client already consumes them. A build without the `truncated` reader caches the clipped copy under staleTime:Infinity and never refetches, so its execution log would stay incomplete until the window is reloaded — the PR even proposed shipping the server half first, which is exactly backwards. Clipping now sits behind MULTICA_CLIP_TASK_MESSAGE_BROADCAST, off by default, so the reader ships first and the flip is a later, revertible ops step. Capability routing would be the principled fix, but the hub does not retain the client_version it is handed at upgrade and frames cross nodes through the Redis relay as serialized bytes — the same protocol work per-task scope routing is already waiting on. 2. Not caching unopened tasks made first open of a live task fetch where it previously found a pre-seeded cache. That fetch replaced the array wholesale, so any frame written while it was in flight was dropped — and with staleTime:Infinity nothing would ever fetch it again. Every write to the timeline cache now folds through one rule, installed as the query's own structuralSharing: union by seq, server data winning on conflict, rows the response never mentioned kept rather than deleted. That subsumes the backfill's hand-rolled eviction, so the three write paths (fetch, backfill, realtime batch) can no longer disagree. Also documents why mobile's truncated branch is unreachable today: its task:message handler gates on chat_session_id, which the server's payload does not carry, so mobile appends nothing at all. Pre-existing, reported separately, and the branch stays so whichever side is fixed first is not left caching clipped rows. Co-authored-by: multica-agent <[email protected]> * fix(realtime): re-check the timeline is still held before writing a batch Holding was verified when a frame entered the batch, but the write happens up to a window later — and `setQueryData` does not postpone garbage collection. query-core arms that timer when the last observer leaves and never re-arms it on write, so closing a transcript while its run keeps streaming has the entry collected mid-window with a batch already in flight. Writing then rebuilt the entry holding only that batch. Under the app-wide `staleTime: Infinity` the next open reads the stub as fresh and never fetches, so everything before it is missing until the window is reloaded — silent, permanent, and not self-healing. Agent runs routinely outlive the 10 minute gcTime, so "opened a running task, closed it, it kept going" reaches this on an ordinary day rather than in a race. The flush now re-checks immediately before each write and drops the batch if the entry is gone, dropping its repair fetch with it. `backfillTaskMessages` guards its own post-await write for the same reason. Dropping costs nothing: the rows are persisted, so the next open fetches the whole timeline — which the regression test asserts rather than just checking the entry stays absent. Also corrects the doc comment on isTaskMessageTimelineHeld, which described the entry as a mounted view. It means "opened at some point and not yet collected", and the bounded tail is one gcTime. Co-authored-by: multica-agent <[email protected]> * docs(realtime): describe the task:message gate as it actually behaves The overview still said frames are dropped unless a mounted view holds the task, and that the old behaviour retained a transcript for the life of the run. Neither matches the code: the gate is timeline-entry presence (opened at some point, not yet collected), and writes do not postpone GC, so retention is bounded by gcTime rather than the run. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f320af4a5b |
MUL-6333 fix(transcript): state the run's agent once, not on every prose row (#7128)
* fix(transcript): state the run's agent once, not on every prose row (MUL-6333) #7011 gave each agent prose row an avatar and a semibold name to promote prose over tool calls. But a transcript is one run by one agent: `agentName` and `agentId` were passed the same values for every row, and `TraceMessageStep` carries no per-step actor, so the block was a constant repeated once per step. The name rendered at `text-caption font-semibold` — the same 12px as `.transcript-prose` body text, at heavier weight. On a run of short steps that made each row's heaviest element its least informative one, and the header 40px above already stated both values. Reported in #7125 with a 20-character agent name over one-line steps; invisible with a short name and long bodies. Drop the block. The existing `bg-success` rule is what marks prose as the agent speaking, and it says so in 2px instead of 48px. Prose promotion is unaffected: that comes from `RichContent` at reading measure staying expanded, not from the avatar. `agentName`/`agentId` were only ever read by `ProseRow`, so the props come off `TranscriptRowProps` too. This restores the pre-#7011 identity behaviour (header only) while keeping everything else that redesign introduced. Co-authored-by: multica-agent <[email protected]> * fix(transcript): give the Agent facet a glyph its rows carry (MUL-6333) Dropping the per-row avatar and name left prose as the only step kind with no mark at all: tool rows print their tool name, thinking and error rows print their kind, and prose printed nothing. So the `Agent` facet in the filter named a set of rows the reader could not pick out — you had to select it to find out what it meant. `StepIcon` already returned a `Bot` glyph for the text kind; nothing rendered it, because `ProseRow` skipped the icon column entirely. Render it. That is the kind mark, not the identity: it says "the agent's own words", which is exactly what the facet selects, and it costs 12px in a column every other row already fills — so prose bodies now align with tool labels instead of hanging left. Carry the same glyph into the filter menu so each facet looks like its rows, and stop labelling tool facets with the raw `tool:Bash` key while the rows they select say `Bash`. The key is unchanged — it is what persists — only the label is. That mislabel predates the rebuild (it goes back to #4884); it is fixed here because it is the same "the filter does not say what it selects" defect. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2a643cc740 |
MUL-6291: stop paying for jsdom in pure-logic frontend suites (#7081)
MUL-6291. Vitest's own breakdown for `@multica/views` showed the shape of the problem: environment 633s and import 644s against only 508s of actual test execution. Two thirds of a cold frontend-test run went on building a jsdom per file and importing its module graph, not on running assertions. - 94 pure-logic `.test.ts` files across views/web/desktop now declare `// @vitest-environment node`. Measured on the 45 converted views files: environment 53.15s -> 4ms, wall 8.72s -> 2.03s. Candidates were picked by module-graph analysis (skipping type-only imports) and rejected when the code under test branches on `typeof window`/`document`, where node would silently take the SSR path and still pass; the remainder were verified empirically and reverted where they failed. - The three jsdom setup files bail out when there is no DOM, so a node-env suite can share them. - schedule-editor: the pure state transitions (isFullDay, displayWindow, timeAnchorOf, defaultAtTime, defaultEveryTime, clampWindow, toggleDay) move out of the component into transitions.ts, with the full combination matrix as a table-driven node-env suite. 34 cases in 3ms, replacing four DOM mounts. - Cross-layer duplicates removed where a canonical layer already owns the matrix: originSourceUrl (skills/lib/origin.test.ts) was re-run through the refresh dialog and the detail page; needsSourceBackfill (core/onboarding/needs-backfill.test.ts) through the backfill modal. Each component keeps one wiring case naming the canonical file. No behaviour change: the transitions extraction is a pure move, and every removal names where the same contract is still covered. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9d6c0c81e0 |
MUL-6254: recover auth after transient Desktop startup failures (#7058)
* fix(auth): recover transient desktop startup failures Co-authored-by: multica-agent <[email protected]> * fix(auth): address recovery review feedback Co-authored-by: multica-agent <[email protected]> * fix(auth): harden web recovery states Co-authored-by: multica-agent <[email protected]> * fix(auth): centralize workspace readiness Co-authored-by: multica-agent <[email protected]> * fix(auth): polish workspace recovery Co-authored-by: multica-agent <[email protected]> * fix(desktop): align workspace readiness after main merge Co-authored-by: multica-agent <[email protected]> * fix(views): clear transcript copy feedback timers Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
09cd442c62 |
fix(views): settle the run timeline's scrollbars so hovering a bar cannot shake it (#7067)
The two-lane strip gave itself 3px of horizontal and 2px of vertical scrollable overflow, which is enough to summon both scrollbars onto 50px of track. Each one changes the box that decides the other, so the layout has two answers and no fixed point; a `:hover` inside re-runs the decision, and the strip flips between 69px and 80px tall for as long as the pointer rests on a bar. Long runs hit it because their trailing segment is a sliver — that is the bar whose 3px minimum lands past 100%. Remove both overflows: clamp a segment's offset by its own width so a widened sliver stays on the axis, and pin `overflow-y` to hidden, since zoom scrolls the track sideways and nothing ever scrolls it down. Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
42dbf60dc8 |
feat(views): rebuild the run trace around steps, lanes, and outcome (#7011)
* feat(views): rebuild the run trace around steps, lanes, and outcome The transcript rendered the raw event stream: a tool call and its result were two rows, so a 75-call run listed 150 lines and the result line was where raw base64 screenshots and 8KB command dumps landed. The bar above it sized its segments by event count, not time, so a 28-minute run drew a barcode that answered nothing. Agent prose — the one thing worth reading — rendered at the same weight as a `Read`, and three density modes existed to compensate. Four layers now, one expanded at a time: - Header keeps status, agent, trigger, duration and spend. - Outcome row names what the run produced: files touched with +/- counts, commands run, delivered branch. Derived from calls we already persist; no comment count, which would have to be guessed from command text. - Two lanes (model / tools) on a real wall-clock axis, each carrying its own total. Model turns and tool calls never overlap in an agent loop, so on one row they could only occlude each other; split, "was it the model or the commands" reads without hovering. The lane pair costs the vertical space the old single track did, and short runs (< 8 steps or < 60s) skip it entirely. - Steps: one call per row with its result folded in, consecutive same-tool calls grouped, prose promoted to body text, and detail in an inspector that opens on selection rather than splitting the surface up front. Pairing is positional because the events carry no call id: `agent.Message` has `CallID` down to the daemon, but `TaskMessageData` drops it before the report and `task_message` has no column for it. A result belongs to the oldest open call with the same tool name until that lands. Colour carries state, not taxonomy — the token set has one blue lightness ramp and no categorical palette, so the lane says what kind of work it was and colour is spent on delivered and failed. Removes the density preference: agent prose reads open and tool calls fold, so the setting has nothing left to switch. Co-authored-by: multica-agent <[email protected]> * fix(views): keep shell commands unfolded and name what a group touched Screenshotting the real surface with a seeded 28-minute run showed two things the fixtures could not. Five consecutive `Bash` calls folded into "Bash · 5 calls", hiding `pnpm typecheck`, `pnpm exec playwright test`, `pnpm lint` and the commit behind one line — the commands are exactly what a reader opens a run for, and the group's duration spanned the model time between them, reading as one ten-minute command. Shell calls no longer group, keyed on the input carrying a `command` string rather than a tool-name allowlist. A folded group also showed only its tool and count, so "Read · 4 calls" said nothing about which files. It now leads with the first call's argument, which is what the approved design showed. Co-authored-by: multica-agent <[email protected]> * feat(views): make the run timeline zoomable and horizontally scrollable Fit-to-width is the right default but it is a lie about short calls: across a 28-minute run, a 2-second command is under a pixel and only renders at all because of the 3px floor. Anything a reader wants to inspect at the tail of a long run is a sliver. The track now scrolls. Zoom steps 1x/2x/4x/8x stretch the lane content past the container while the lane labels stay pinned — a total that scrolls away is a total you cannot read against its bars. Tick density scales with zoom so the axis keeps its on-screen spacing instead of thinning out, zooming holds whatever was under the viewport's centre, and selecting a step scrolls its playhead into view. The control sits out of the way until hovered, and stays visible once zoomed so the way back is never hidden. Also answers "is two lanes all there is": the tool lane's time now breaks down by what the calls did (commands / edits / reads / other) in the lane's tooltip. It stays a tooltip rather than three more lanes because in a real run one kind dominates by orders of magnitude — a lane per kind would draw three near-empty tracks to say what one line of text says. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c9727e6b2b |
feat(daemon): worktree mode for local_directory resources (MUL-5707) (#6759)
* feat(daemon): worktree mode for local_directory resources (MUL-5707) Local directory resources serialise every task on a per-path mutex, so two tasks on one folder queue behind each other. This adds an opt-in worktree mode: each task gets its own git worktree of the user's repo inside the daemon's env root, so tasks run concurrently and deliver their work as a branch instead of editing the user's working copy. Opt in per resource with execution_mode=worktree (default stays in_place, so existing resources are unchanged). Requires a git repo with at least one commit; a non-git directory fails with an actionable message rather than silently falling back to serial execution. Design points that make the mode safe to use: - The agent starts from what the user sees, not HEAD. Tracked edits are captured with `git stash create` (which writes a commit object without touching the index, working tree, or stash list) and replayed into the worktree; untracked non-ignored files are copied, bounded at 2000 files / 200MB with the skipped count logged at warn level. - That replayed state is committed as a baseline, so "did this task change anything?" has an exact answer and `git diff <baseline>..<branch>` is precisely the agent's work. - Nothing is silently discarded: leftovers are committed to the branch before the worktree is removed, including when the task fails partway. - A task that changes nothing leaves no branch behind. - Sidecars stay out of the branch by running the existing CleanupRuntimeConfig + CleanupSidecars pass over the worktree before finalizing. The .git/info/exclude approach repocache uses is not usable here: a linked worktree resolves info/exclude to the user's own common git dir, so it would change what `git status` hides in their checkout. - Worktree-mode env roots do not get the local_directory GC exemption, so they age out normally instead of accumulating one env root per task. - `git worktree prune` on create self-heals registrations orphaned by a crashed daemon. Adds --execution-mode to `project resource add/update`, and updates the docs and the projects-and-resources built-in skill. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close data-loss and isolation gaps in worktree mode (MUL-5707) Review follow-up on #6759. Six issues, all on failure or skew paths. 1. Preserve work when the final commit fails. Finalize logged a warning and then ran `git worktree remove --force`, destroying the agent's changes. commit.gpgSign with no key available to the daemon triggers this, and --no-verify does not disable signing. Finalize now returns an error and deliberately keeps the worktree — it stays registered, so `git worktree list` points at the work — and the daemon fails the task instead of reporting success with a branch that is missing the changes. 2. Fail closed on an unsupported execution_mode. Treating an unknown mode as in_place was wrong: execution_mode is how a user asks for ISOLATION, so the "safe" fallback would edit the working copy they asked us to stay out of. An unrecognised mode now fails the task naming the version skew. 3. Keep Codex conversations alive across turns. The per-issue session store was keyed on LocalWorkDir != "", so worktree tasks — which also never reuse an env root — got a fresh empty CODEX_HOME per turn and silently lost the thread. Covered by a two-turn regression test that fails without the fix. 4. Fail closed on an incomplete snapshot. A failed stash create/apply, a failed untracked copy, or hitting the copy bounds all degraded to "agent sees HEAD" with only a daemon log line. A half-replayed tree looks like a working checkout, so nothing downstream questions it while the agent reads different code than the user has. All of these now abort the prepare and tear the partial worktree back down. 5. Do not snapshot a directory mid-mutation. The same real path can be attached to another project as an in_place resource; snapshotting under a running in_place task captured a half-written tree plus that task's in-flight .agent_context/.multica sidecars, which then reached the branch and could feed the agent another issue's brief. Worktree prepare now takes the per-path mutex for the snapshot alone (released before the run, so tasks still overlap), and the untracked replay skips Multica's own sidecars. 6. Stop the test helper from masking regressions. gitRun skipped on any git failure, including post-finalize assertions, so a lost branch could report as a skip. Git availability is now checked once at repo setup; every later git failure is fatal. Also rebased onto main, keeping both MulticaConfigRoot and LocalWorktree in the Environment initialiser. Co-authored-by: multica-agent <[email protected]> * fix(daemon): close sidecar-commit and wait-visibility gaps in worktree mode; gate worktree saves on daemon version (MUL-5707) - runTask: register the sidecar-cleanup defer immediately after the Finalize defer. Previously two early returns (temp-dir setup, StartTask failure) sat between them, and on those paths Finalize auto-committed the sidecars Prepare had just written — delivering a branch whose only content is Multica's own runtime files (in_place: leaving them in the user's tree). - runTask: give the worktree snapshot wait the same plumbing as the in-place acquire — waiting_local_directory status, resourceWaitTasks accounting, and a cancellation poller. The holder can be an hours-long in_place task; the wait was invisible and a user cancel went unnoticed until the 5-minute prepare timeout. - isMulticaSidecarPath: match sidecar dirs as whole path segments at any depth, not only at the repo root. An in_place resource pointed at a SUBDIRECTORY leaves its sidecars below that subdirectory; replaying them committed another issue's brief into the delivered branch. - handler: refuse saving execution_mode=worktree while the owning daemon's registered cli_version is below MinLocalWorktreeCLIVersion (422, daemon_version_unsupported). An old daemon json-skips the field and runs tasks IN PLACE — the daemon-side unknown-mode refusal cannot protect a daemon that predates the field, so the server has to. Residual (documented): a daemon downgraded after the save needs a claim-time gate. - copyUntrackedFiles: read the -z listing raw instead of TrimSpace-ing it, which corrupted filenames beginning/ending with whitespace into paths that silently vanished from the replay. - docs + skill: describe the save-time version gate; drop the stale claim that old runtimes fail the task themselves. Co-authored-by: multica-agent <[email protected]> * feat(views): UI switch for local_directory execution mode (MUL-5707) Worktree mode shipped CLI/API-only, which for its target user — someone running several tasks at once from Desktop — means it does not exist. This adds the switch to the resources panel. The choice is framed by what the user gets back, not by the mode identifiers: "Edit this folder directly" (one task at a time, changes land in your working copy) vs "Run in parallel, isolated" (tasks run at the same time and each hands back an agent/… branch). `in_place` / `worktree` stay visible as a secondary hint for anyone cross-referencing the CLI or docs. Two entry points, matching where the decision is actually made: - The add-a-folder flow now asks for the mode after the folder is picked and validated, instead of creating the resource and leaving the mode to be discovered later. - Every local_directory row has a mode control. It is deliberately NOT gated on being the owning machine — switching the mode only rewrites a field, so it works from the web app too, unlike rename or the folder picker. Both blocked states are handled where the user chooses, not after they commit: - Non-git folder: the desktop validator now reports whether the path is inside a git working tree (walking up for `.git`, so a subdirectory of a repo counts), and the option is disabled with that reason. Unknown — an older desktop build, or an existing row — stays permissive; the daemon re-checks authoritatively. - Daemon too old: the version floor is mirrored from the server as MIN_LOCAL_WORKTREE_CLI_VERSION and checked against the runtime for the resource's OWN daemon, not the machine the browser happens to be on. A server rejection is rendered inline in the dialog rather than as a toast, so the dialog stays actionable. A "Parallel" badge on the row is always visible (not hover-only): without it there is no way to tell whether tasks on a folder edit it directly or hand back a branch, which is the first thing anyone asks when a task queues. Also adds `execution_mode` to LocalDirectoryResourceRef — the field existed server-side but had no TypeScript counterpart — and brings the zh/ja/ko project-resources docs in line with the English ones, which were the only locale updated when the mode landed. Co-authored-by: multica-agent <[email protected]> * fix(daemon,handler): close worktree isolation and delivery gaps (MUL-5707) Second review round. Five must-fix items, all on paths where the mode's promises could still break. 1. A failed sidecar cleanup no longer ships Multica's own files as the user's branch. The cleanup errors were logged and Finalize then ran `git add -A`, so a permission/disk/lock failure could deliver a branch whose entire content was the runtime brief and issue context. Worktree mode now escalates a cleanup failure to an abort: nothing is committed, the worktree is kept for inspection, and the task fails with the preserved path. In place it stays a warning — no commit, no branch, and the leftovers are visible in the user's own tree. 2. The untracked replay no longer drops files silently. `Lstat` failures and symlinks were `continue`d without counting, so the caller read a partial snapshot as complete. Both now count as skipped, which the existing fail-closed check turns into a refused prepare. Non-content entries (sockets, FIFOs, devices) stay uncounted deliberately. 3. Prepare rolls back a worktree it created. Any later failure — context files, provider home, MCP config — returned without an Environment, so nothing downstream knew a worktree existed, leaving a registration and a branch no task ever ran in. A deferred Discard now runs unless Prepare reached its successful return. 4. The daemon-version gate moved to where it can actually hold. Save-time validation cannot see a machine downgraded afterwards, and an old daemon json-skips execution_mode entirely — it would run the task in place, editing the working copy the user asked to isolate. Claims are now checked against the runtime that is actually claiming, and a blocked task is cancelled with a reason rather than left to be redelivered to the same too-old runtime forever. 5. branch_name is delivered end to end. The daemon sent it, but TaskCompleteRequest had no field so it vanished at decode, and the failure path never sent it at all. It now has a column, travels on both terminal callbacks, and renders as a copyable row in Run details. The failure path matters most: worktree mode commits the agent's leftovers before tearing the worktree down, so a run that died partway still has work worth finding. Docs (all four locales) and the projects-and-resources skill are updated for the claim-time gate and the branch surface. Co-authored-by: multica-agent <[email protected]> * fix(handler,daemon): migration renumber, cancel reasons, branch on every terminal path (MUL-5707) Third review round. 1. Rebased onto main and renumbered the migration to 285 — main had taken 273 and is now at 284. sqlc regenerated on top of the rebased queries; the conflicts were confined to generated files. 2. The claim gate now persists why it cancelled. It called CancelTask, which writes only status='cancelled', and returned the reason as a 4xx that reaches the daemon's log and nothing else — batch claim drops the task from its response entirely. A run refused for a durable reason the user has to act on cannot present as a bare "cancelled", so this adds CancelAgentTaskWithReason and records error + failure_reason on the row. 3. branch_name now survives every terminal path, not just the two direct callbacks. Three were dropping it: - the context-exhausted complete→fail reroute did not copy it; - the daemon's complete→fail fallback (server rejected the complete, agent had succeeded) did not set it; - both cancellation paths discarded the result wholesale, even though the worktree is finalized — and its work committed — before the daemon learns of the cancel. The branch now rides the cancel-ack and is recorded by SetAgentTaskBranchName, which never overwrites a name a terminal callback already wrote. 4. Rollout/rollback compatibility. Isolation is enforced by two server-side gates, so a daemon downgraded at any point after the resource was saved is caught. The gap they cannot close is rolling the SERVER back past this change while sub-floor daemons are still connected: an old server has no gate and an old daemon ignores execution_mode. That constraint is now documented where an operator will meet it (the migration and the docs), and the version floor is pinned across server and frontend by test so the two gates cannot silently drift apart. Co-authored-by: multica-agent <[email protected]> * fix(server,views): route claim-gate cancels through TaskService, make cancel reasons visible, harden cancel-ack branch delivery (MUL-5707) Closes the fourth-round review findings on worktree mode's cancellation chain: - Claim gate: cancel through a new TaskService.CancelTaskWithReason instead of a raw CancelAgentTaskWithReason query, so a gate-cancelled task gets the full cancellation side effects — audit capture, chat settle, agent status reconcile, task:cancelled broadcast, NotifyTaskFinished. Added REAL claim integration tests: singular claim (422 + row cancelled with reason), batch claim (task omitted + row cancelled with reason), and a current-runtime control. - Cancel reasons are now user-visible: execution log rows, agent activity rows, and the transcript dialog label a SERVER-cancelled task (persisted error/failure_reason) like a failed one, with the actionable text on hover and a Reason row in Run details. User-initiated cancels stay a plain "Cancelled". New cancelReasonLabel helper + local_directory_error label; details_reason locale key in en/zh-Hans/ja/ko; widened AgentTask.failure_reason to the open string the wire actually carries. - Cancel-ack is now a terminal delivery: the daemon retries it on transient errors (same schedule as complete/fail), the server fails LOUD (5xx) when persisting the branch/error fails instead of warn-and-200, and re-broadcasts task:cancelled after a successful write so clients that already refetched a branchless row pick up the delivery. Replays are idempotent (never overwrite). - Cancel + Finalize-abort no longer swallows the preserved-worktree pointer: the finalize error is wrapped in worktreePreservedError (joined onto an existing run error, never displacing it), extracted on the cancel path, and delivered via the ack into agent_task_queue.error through the new SetAgentTaskErrorIfEmpty query — never overwriting a reason recorded by a fail callback or the claim gate. Other run errors on a cancelled run stay discarded as before: they are expected noise. - Docs: the self-hosted rollback constraint callout now exists in zh/ja/ko, not just English. Co-authored-by: multica-agent <[email protected]> * fix(handler): guard cancel-ack writes to cancelled rows; requeue when the claim gate's cancel fails (MUL-5707) Fifth-round review fixes: - SetAgentTaskBranchName / SetAgentTaskErrorIfEmpty now carry a status='cancelled' CAS. The daemon acks on EVERY terminal status it observes, so a late or replayed ack from a stale run could previously backfill its branch or preserved-path error into a completed/failed row whose own callback — the authoritative channel — recorded nothing, without any event invalidating the change. A CAS-refused write is a deliberate no-op (still 200: nothing to retry), and the rebroadcast stays guarded by the same status check. Regression tests: a late ack against a completed row with empty fields and against a failed row leaves both untouched. - When the worktree claim gate's CancelTaskWithReason fails, the claimed row no longer strands in dispatched (daemon refused, nothing to release it but stale reclaim). The gate requeues via RequeueTaskAfterClaimFailure — the next claim re-runs the gate and retries the cancel — and reports 5xx so the singular daemon reads a transient server problem, not a claim refusal; the batch path omits the task as before, now with the row requeued. Fault injection tests (row-scoped trigger raising on the cancel UPDATE while letting the requeue through) cover singular and batch. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3dd128532c |
feat(agent): add oh-my-pi (omp) as a supported agent runtime (#6514)
* feat(agent): add oh-my-pi (omp) as a supported agent runtime oh-my-pi (omp, https://omp.sh) is a separate CLI that is a drop-in fork of pi and speaks the same JSON event protocol. It is registered as an independent provider so a host with both pi and omp installed gets two runtimes instead of one. omp is a runtime identity on the pi protocol, NOT a new protocol family. It is declared in a single BuiltinRuntimes descriptor (server/pkg/agent/builtin_runtimes.go) that carries id, protocol_family, default_command, env_prefix, display_name, skills_dir, user_skills_dir, launch_header, backend overrides, and model_discovery strategy. Every consumer (agents_probe.go, config.go, daemon.go, execenv/context.go, local_skills.go, agent.go New/LaunchHeader, models.go ListModels) derives from this descriptor — no per-file hardcoded omp special cases. NewRuntime() is the typed entry point for runtime identities: it resolves id → descriptor → protocol family backend → applies overrides through a backendOverrideApplicator interface (piBackend implements it). New() delegates to NewRuntime() for built-in runtime identities, keeping New() meaning exactly one thing: the protocol-family factory. Model discovery: parseOmpModels reads the real {"models":[...]} wrapper with separate provider/id/selector/name fields. Model.ID is the selector (provider/id), matching parsePiModels' convention so buildPiArgs emits both --provider and --model — not just --model. A regression test (TestOmpSelectorSurvivesToBuildPiArgs) pins this end-to-end. Frontend: provider-logo.tsx maps omp to PiLogo. The transcript dialog uses the shared providerDisplayName helper. Landing copy updated to 21 tools in all 4 locales. Tests (omp_test.go): descriptor dispatch, NewRuntime entry point, binary resolution, omp-labeled errors, event-stream completion, pi+omp side-by-side registration, selector→buildPiArgs regression, model parser coverage (real JSON/empty wrapper/invalid stderr/duplicate), and a guard that every descriptor field is non-empty and consumed. Closes #3989 * fix(views): keep Claude Code label in the transcript run details Switching the provider row to the shared runtime formatter renamed every Claude run: the daemon has no display-name override for claude, so the shared formatter answers "Claude", and the legacy claude-code value title-cased into "Claude-code". Both read as "Claude Code" before. Keep those two aliases in a table local to this view and defer everything else to the shared formatter, so the row stays in lockstep with the runtime list (#5260) without renaming Claude runs. * test(daemon): assert pi and omp both reach the register payload The existing omp test stopped at probeAgentCLIs, so nothing covered the part a user actually sees: whether both runtimes are registered with the server, each under its own display name. Discovery finding two entries and New() building two backends both stop short of version detection and the registration payload. Add a daemon-level test that runs real discovery off a fake PATH, drives syncWorkspacesFromAPI, and asserts the payload carries type=pi and type=omp with names "Pi" and "Oh-My-Pi". Rename the discovery test to say what it covers, and record the registered display name in the batch fixture so the name can be asserted. * docs(agent): correct descriptor comments that describe the old behaviour Three doc comments still described behaviour the fail-closed rework replaced: - ModelDiscovery claimed a nil strategy falls back to the family's discovery; ListModels deliberately returns an empty catalog instead, because omp exits non-zero on pi's --list-models. - backendOverrideApplicator claimed backends without it are returned unchanged; NewRuntime returns an error rather than dropping the descriptor's executable and label. - ProtocolFamily credited New() with applying the ID-specific defaults; that moved to NewRuntime. Also repair the truncated first sentence of the NewRuntime doc, and fix discoverOmpModels still calling the output a JSON array when parseOmpModels right below it documents the {"models":[...]} wrapper. * style(server): gofmt the files this branch touched Four files were left unformatted: the descriptor literal and the omp test struct lost their key alignment, execenv/context.go had its new import out of order, and local_skills.go kept the old indentation after its switch moved inside an if/else. Whitespace only — no behaviour change. --------- Co-authored-by: Bohan-J <[email protected]> |
||
|
|
ba129b1963 |
feat(issues): show per-run token usage on the execution log (MUL-5762) (#6440)
* feat(issues): show per-run token usage on the execution log (MUL-5762)
The execution log already lists every agent run on an issue; it just
never said what any of them cost. task_usage has held per-task token
counts since migration 032 — nothing surfaced them per run.
Three placements, one data source:
- Execution-log header carries the issue total ("2.1M · $4.92") and
opens the breakdown.
- Each row carries its own token figure. This takes the slot the
relative timestamp held: the sidebar is 288px and a third column
would come out of the trigger text, which is what people scan. The
list is sorted newest-first, so ordinal recency is already free; the
timestamp moves into the row tooltip alongside duration and model,
neither of which was surfaced there before.
- The transcript dialog gets the same figure in its header, with the
input/output/cache split in the run-info popover.
Backend: ListIssueTaskUsage returns per-(task, provider, model) rows in
one query, joined onto the existing task-runs response. The model
dimension stays on the wire because cost is priced client-side per
model — a row that collapsed two models cannot be priced at all.
Cost reuses estimateCost from the runtime usage page, so the issue and
the workspace never disagree; the new summarizeTaskUsage helpers live
next to it rather than starting a second cost formula.
No usage recorded stays distinguishable from zero end to end — omitted
on the wire, undefined in the schema, null from the summarizer, an em
dash in the UI. A run from before usage reporting was not free.
Removes the standalone "Token usage" sidebar section: it showed the
same issue totals minus the cost and minus any way to attribute them,
and every field it had is in the dialog. The /api/issues/:id/usage
endpoint it read stays — the CLI's `issue usage` still uses it.
Co-authored-by: multica-agent <[email protected]>
* fix(issues): address review on per-run token usage (MUL-5762)
Three findings from @Emacs, all confirmed against the code:
1. Drop the token figure from active rows. The daemon reports usage
once, after `runner.run` returns (internal/daemon/daemon.go), and
ReportTaskUsage publishes no realtime event — so a running task has
no usage to show and would not learn of it mid-run if it did. The
branch was only ever exercised by a hand-written fixture, which is a
test asserting a scenario production cannot produce. The row keeps
its timer; restore the figure in the same change that adds
incremental reporting + cache invalidation.
2. Subscribe the usage surfaces to the custom-pricing store. estimateCost
reads custom rates imperatively via getCustomPricing(), so nothing
re-rendered these after a saved rate change — the header total, the
dialog's totals and per-run costs, the cost-by-agent split, and the
"unmapped model" notice all kept quoting the old price until the task
list happened to refetch. Same subscription the runtime usage page
already carries, plus the snapshot in every memo that prices usage.
Regression test pinned: it fails without the subscription.
3. Give the dialog's status glyph an sr-only label. TaskStatusIcon is
aria-hidden, so a screen reader could not tell a failed run from a
completed one — the execution log rows already pair the two.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
e422b169a8 |
MUL-5415: fix(issues): keep running-agent transcript open
Closes #6053 |
||
|
|
9c90327ce4 |
fix(ui): replace the text-transparency ladder with solid tones (MUL-5452) (#6152)
* fix(ui): replace the text-transparency ladder with solid tones (MUL-5452) Hierarchy was being expressed with transparency: 152 call sites of text-muted-foreground/30..80, 26 of text-foreground/60..90, plus a handful on destructive and current, and a few written as a standalone opacity-* utility instead of a slash alpha. On light surfaces every muted variant failed WCAG AA - /80 reached only 3.78:1 and /40 sat at 1.80:1, below even the 3:1 floor for non-text - because the palette had no step below --muted-foreground, so transparency was the only tool for 'quieter than muted'. The palette now has that step, and it is deliberately non-text: --faint-foreground clears 3:1 (WCAG 1.4.11) on every surface for icons, chevrons, separator glyphs and empty-cell em dashes. There is no room for a third readable text tone - AA caps a lighter text tone 0.018 L away from muted - so text keeps exactly one floor, --muted-foreground. Also fixes text-destructive/70 on a cron error message, which was 3.61:1. This branch changes zero font sizes. The sub-12px half of the issue is MUL-5451's (#6136); keeping the two apart is what makes this one reviewable on its own after #6108 was reverted. apps/web/app/text-contrast.test.ts replaces muted-foreground-contrast.test.ts rather than sitting beside it. It recomputes the floors from tokens.css instead of hard-coding ratios, and fails the build on all four ways to spell the defect: /70, /[0.5], /[50%], and a detached opacity-* in the same class string. Transparency behind hover/focus/disabled stays allowed - the resting state carries the contrast obligation and it is solid. Co-authored-by: multica-agent <[email protected]> * fix(ui): correlate transparency across a whole class expression Review found two ways past the guard, both real. A per-literal check cannot see cn("… text-muted-foreground", suppressed && "opacity-60") - one element wearing a colour in one argument and a dim in the next. That split shape is the common one, and it was hiding live violations: the comment trigger chips dimmed aria-pressed label text to 2.55:1 while the sweep reported clean. The second was my own exemption. Accepting any state word within 80 characters let "text-muted-foreground hover:text-foreground opacity-50" through, because the hover: belongs to the colour, not to the opacity. The detector now correlates across a whole cn() call or template literal, splits it into segments that each carry their own condition, and exempts only on the variant prefix the opacity utility itself carries or on the condition governing its segment. Segment splitting is what keeps ${disabled ? "opacity-60" : ""} exempt while flagging its neighbours. Fixed what that surfaced: three trigger-chip controls (the suppressed state is already carried by the avatar's own grayscale, the sentence wording and a solid muted step), a disabled-skill icon chip, and the diff gutter marker. tab-bar's isDragging is exempt - a drag ghost is an in-flight gesture, the same category as :active. The detector now has its own table of thirteen cases. Every hole so far has been silent, so the shapes it must and must not catch are pinned next to the reason each one exists. Co-authored-by: multica-agent <[email protected]> * test(ui): cover the faint tone in the cn() merge regression test text-faint-foreground is a new text-<x> class, which is the exact shape that silently broke the sidebar labels in #6108: tailwind-merge cannot tell a size from a colour and drops one of them. The token does resolve correctly today - verified both orders against a size step and against another colour - but the test that exists to catch this was not covering it, so the guarantee rested on nothing. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
999e9f93c7 |
fix(codex): record file edit payload for patch_apply events (#6158)
* fix(redact): scrub secrets nested inside tool input maps and slices
InputMap only passed top-level string values through Text and documented
non-string values as "preserved as-is". Any secret one level down reached
the database and the WebSocket broadcast untouched:
flat -> [REDACTED ...] (scrubbed)
nested -> [map[diff:token=ghp_... path:a.go]] (leaked verbatim)
This is a prerequisite for recording structured file-edit payloads. Codex
reports an edit as changes[]{path, diff, content}, and the legacy protocol
reports a deletion as the whole outgoing file — so without this, deleting a
.env would persist its full contents in cleartext.
redactValue now walks the composite shapes json.Unmarshal produces, plus
[]string and map[string]string for argv-style inputs. Composites are copied
rather than scrubbed in place, because the caller keeps using the map it
passed in.
Nesting depth comes from daemon-supplied JSON, so the walk is bounded at 32
levels; a pathologically nested payload would otherwise recurse until the
stack blows. Hitting the bound yields a placeholder rather than the raw
value, keeping the fail-safe direction.
Verified: the five new tests each fail against the previous top-level-only
implementation and pass now; full ./pkg/redact suite green.
Co-authored-by: multica-agent <[email protected]>
* fix(codex): record file edit payload for patch_apply events
Both Codex protocol paths recorded a file edit as a bare call ID and set no
payload, so a run that edited six files left six blank, unexpandable rows in
the transcript. The same task on Claude or Grok showed a readable diff, and
the branch Codex pushed was the only surviving record of what it changed
(GH #6157).
The omission was specific to this one tool, not to the adapter: the
exec_command handlers directly above already captured command and output.
Both paths are fixed, since the protocol is sniffed at runtime. Their wire
shapes differ more than they appear, and the normalizer reconciles that:
- legacy patch_apply_begin/end carry map[path]FileChange, internally tagged
on `type`, where add/delete hold whole-file `content` and only update
holds a `unified_diff` plus `move_path`. There is no diff for every case,
so the normalized form keeps diff and content as alternatives.
- v2 fileChange items carry an ordered array of {path, kind, diff} where
`kind` is an object, not a string — reading it as a string silently
yields "" and loses the add/delete/update distinction.
- status spellings differ too: legacy is snake_case, v2 is camelCase and
adds inProgress. Both normalize onto one vocabulary, and a legacy event
predating `status` falls back to its `success` bool.
Legacy map iteration is sorted by path so a replayed event does not reshuffle
the file list.
Completion events now also produce a non-empty output (status, file count,
and any apply_patch stdout/stderr), because an empty output renders as an
unexpandable blank row just like a missing input.
Anything unrecognised — absent, wrongly typed, or malformed changes — returns
no payload, preserving exactly the previous degradation rather than risking
the transcript.
Total diff/content bytes are bounded at 64 KiB with UTF-8-safe truncation,
recording `truncated` and `original_bytes`; paths and kinds always survive,
since they are what a reviewer needs when the body is gone. The bound is
deliberately scoped to this new payload: other providers stream tool inputs
through unbounded, and clamping them here would silently truncate
transcripts that render correctly today. Unifying the limit at the
persistence boundary is left as a follow-up.
Verified: the new tests reproduce the reported symptom (Input:map[],
Output:"") against the previous call sites and pass now; ./pkg/agent and
./pkg/redact green, go vet and gofmt clean.
Co-authored-by: multica-agent <[email protected]>
* feat(transcript): render Codex multi-file patch payloads as diffs
The presenter identified an edit by input shape — a top-level file_path plus
old_string/new_string or content — which is Claude's and Grok's shape. Codex
records one patch_apply covering several files as changes[], so even with the
payload now populated it fell through to pretty JSON instead of a diff.
A new `patch` detail kind carries one entry per file, since collapsing them
into a single body would lose which change belongs where. Each file reuses the
existing single-file surfaces, so all bodies behave alike inside the
virtualized list.
Codex hands over a ready-made unified diff, so parseUnifiedDiff maps it onto
diff rows rather than recomputing one — there is no before/after pair to
compare, and reconstructing both sides from the diff just to diff them again
would be circular. Hunk headers become `gap` rows, which is what they denote:
skipped unchanged content.
A deletion renders as all-removals rather than as a whole-file write, because
the legacy protocol reports it as the outgoing file's content and a green
"+N" gutter would state the opposite of what happened.
The collapsed row needed its own fix: with no single path field, the summary
fell through the preference chain and came back empty. It now reads as the
first path plus "+N more".
Anything that is not this shape still falls back to pretty JSON, so a payload
this presenter does not understand stays readable.
Verified: 17 new tests (43 in the presenter suite) pass; repo typecheck and
lint clean. The one failing views test, layout/sidebar-resize, fails
identically on an untouched checkout.
Co-authored-by: multica-agent <[email protected]>
* fix(codex): route v2 add/delete payloads as content, not diff
Addresses review on #6158.
Upstream's format_file_change_diff only produces a unified diff for `update`.
For `add` and `delete` it returns the whole file's contents under the same
`diff` field, and for a moved `update` it appends a trailing
"\n\nMoved to: <path>" line:
FileChange::Add { content } => content.clone(),
FileChange::Delete { content } => content.clone(),
FileChange::Update { unified_diff, move_path } => ...
(codex-rs/app-server-protocol/src/protocol/item_builders.rs, rust-v0.145.0)
Recording that as a diff mislabels every line of an added or deleted file as
context, and actively inverts any line whose content begins with '+' or '-' —
so an added file containing "-minus lead" rendered as a deletion. The payload
is now routed by `kind` rather than by field name, and the "Moved to:"
sentence is stripped since move_path already carries the destination.
The previous v2 tests hid this by using a fixture the real protocol never
emits (an `add` carrying "@@ ... +package main"). They now use upstream's
shape, plus cases for delete, an empty add, and an add whose contents look
like diff headers.
Empty bodies are also kept on both paths: presence of the field, not its
non-emptiness, decides whether a body was reported, so an empty added file
renders as an empty body instead of "no content reported".
Verified: the new assertions fail against the previous normalizer — where an
`add` came through as {"diff": "package main\n"} — and pass now.
Co-authored-by: multica-agent <[email protected]>
* fix(transcript): stop treating header-like content lines as file headers
Addresses review on #6158.
parseUnifiedDiff matched "---" / "+++" / "diff --git" / "index " at any
position, so a changed line whose *content* starts with a dash or plus was
silently discarded:
parseUnifiedDiff("@@ -1 +1 @@\n--- old markdown\n+++ new markdown\n")
// before: [{ kind: "gap", ... }] — both changed lines gone
A removal of "-- old markdown" is spelled "--- old markdown" on the wire, so
this hit Markdown rules, embedded patches, and comment banners.
File headers only exist ahead of the first hunk, so they are only recognised
there; once inside a hunk every line is parsed strictly by its first
character.
Also localizes the multi-file summary count, which was hardcoded English and
so leaked into the zh-Hans / ja / ko transcript rows. The presenter owns no
React and no i18n by design, so the phrasing is injected by the caller rather
than imported here, keeping the module unit-testable in isolation; the English
form remains the fallback. The three Chinese/Japanese/Korean truncation
strings now use "..." to match the English source they translate.
Verified: both new parser assertions fail against the previous
strip-anywhere behaviour and pass now; 47 presenter tests green, repo
typecheck and lint clean.
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): redact nested tool input before it leaves the daemon
Addresses review on #6158.
Recursive redaction ran only in the server's ingest handler. The daemon built
the new nested edit payload and sent msg.Input verbatim, so a daemon that
self-updated ahead of the server — or one talking to a server mid-rollout —
would ship whole-file edit contents to a peer that does not scrub nested
values yet. The legacy protocol reports a deletion as the whole outgoing file,
so that window covered a deleted .env in cleartext.
Ordering three commits inside one PR is not a deployment barrier, and daemon
and server upgrade independently. Deployment order is not a control we have,
so the sending side is now safe on its own; the server keeps redacting on
ingest as the second line of defence.
Scoped to Input, which is the field this PR newly fills with file contents.
Content and Output are plain strings already redacted server-side, and
changing their daemon-side handling would be unrelated to this fix.
Verified: the new daemon test asserts the nested token is masked in the
reported batch while the change metadata survives. It fails without this
change, reporting the full GITHUB_TOKEN= line on the wire, and passes with
it; ./internal/daemon green.
Co-authored-by: multica-agent <[email protected]>
* fix(transcript): correct the Chinese multi-file patch count semantics
Addresses review on #6158.
The summary is handed the number of files *beyond* the named one, but the
Chinese phrasing stated a total: "a.go 等 2 个文件" reads as two files including
a.go, so a three-file patch under-reported by one. English hides the
distinction ("+2 more"), which is why it survived the first pass.
Rewords zh-Hans to "另有 N 个文件". Japanese (他) and Korean (외) already read
as "besides", so their wording is unchanged.
Also renames the interpolation variable from `count` to `extra`, for two
reasons. i18next treats `count` as the plural selector — this very namespace
relies on that for events_one/events_other — so a plain number had no business
borrowing it. And the name is what a translator reads: `extra` cannot be
mistaken for a total the way `count` was.
Guards the whole bug class rather than just this string: a locale test asserts
every locale interpolates {{path}} and {{extra}} and never the reserved
{{count}}, and a presenter test pins that the injected number is the count of
additional files, not the total.
Verified: both new locale assertions fail against the reverted string and pass
now; rendering the real locale strings for a three-file patch yields "+2 more",
"另有 2 个文件", "他 2 件", "외 2개". 53 target tests pass, repo typecheck clean,
views lint back to its pre-existing 16 warnings and 0 errors.
Co-authored-by: multica-agent <[email protected]>
* fix(transcript): put the patch surface on the type scale
Addresses review on #6158.
The patch surface wrote text-[10px] / text-[11px] / text-[10px], copied from
the sibling transcript surfaces as they looked when this branch started. Since
then MUL-5451 (#6136) introduced a role-named type scale and migrated those
same siblings to text-micro, so these three call sites were the only remaining
arbitrary sizes — and the type-scale guard reports them precisely.
All three become text-micro. That matches the analogues they were copied from
now that those have moved: the FileWriteSurface line-count row, the
DiffDetailSurface header row, and the ToolDetailSurface body. It is also the
only correct target, since micro (11px) is the smallest step the scale defines
— there is nothing at 10px to map to.
Merges origin/main so the guard runs here rather than only in CI.
Verified: apps/web app/type-scale.test.ts 13/13 (it listed exactly these three
lines before), no `text-[` left in the file, repo typecheck clean, views lint
unchanged at 16 pre-existing warnings and 0 errors.
Co-authored-by: multica-agent <[email protected]>
* fix(codex): redact patch bodies before applying the size budget
Addresses review on #6158.
The adapter sized and truncated the normalized changes, and redaction only ran
later — in the daemon before sending, then again in the server on ingest. That
order loses secrets that straddle the budget.
The PEM rule needs both markers to match:
-----BEGIN[A-Z\s]*PRIVATE KEY-----.*?-----END[A-Z\s]*PRIVATE KEY-----
So a 70 KB private key whose BEGIN sits inside the first 64 KiB and whose END
falls past the cut stops matching once truncated. Neither later pass can
recognise what truncation already broke, so the marker and 64 KiB of key
material reach the database and the WebSocket broadcast. Measured on the
previous code:
stored bytes 65536 | BEGIN marker present | key body present | placeholder absent
Redaction now runs first, and the budget measures the redacted bodies — which
is also the honest measurement, since those are what actually gets stored and
redaction usually shrinks them (that key collapses to 23 bytes, so no trimming
is needed at all). `original_bytes` still reports the pre-redaction size so the
reader sees how large the real patch was. The daemon and server passes stay as
defence in depth; redaction is idempotent, so running three times is safe and
that is now asserted.
Note for callers: codexPatchInput no longer trims its argument in place, because
redaction copies first. Two existing tests were asserting on the caller's
original slice and had silently become vacuous; they now read the returned
payload, and one pins the no-mutation contract. The delete fixture in the
diff-vs-content routing test was also a credential-shaped string, which now
redacts — it is plain text so that test keeps testing routing.
Verified: the new boundary test fails on the previous order, reporting the
surviving BEGIN marker and key material, and passes now. go test ./pkg/agent
./pkg/redact ./internal/daemon green; execenv ByteIdentical green; go vet and
gofmt clean.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
7803a5b9ea |
feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6136)
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 written as arbitrary values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the ~1,900-call-site rename moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: 8/9/10px -> micro (11px) on badges and overlines; 17 -> 18; 22 -> 24; 30 (text-3xl) -> 36 on headings and stat numbers; 12.8px -> label (13px) on small buttons and toggles. Half-pixel sizes are gone. This supersedes #6108, which was reverted by #6116 because the sidebar group labels rendered at the inherited 16px. The cause was not the scale but cn(): `text-<x>` is ambiguous in Tailwind, and tailwind-merge resolves it against a table listing only the default sizes, so it filed every role step under text-colour and dropped whichever of `text-caption` / `text-sidebar-foreground/70` came first. Registering the steps as a font-size class group restores the real conflict groups — size beats size, colour beats colour, the two coexist — and a test pins the list against the scale, since the failure is silent in source. Hand-written CSS is covered too. The transcript kept a 12.5px body long after every Tailwind call site was on the scale, so the "no half-pixel sizes" claim was true of the classes and false of the product; the editor's prose, code and mermaid ramps had the same blind spot, and seven of their eight values already equalled a step exactly. All now reference var(--text-*). The guard test reads raw `font-size:` declarations as well as class names, exempting only the 16px iOS input-zoom workaround in base.css and the landing pages' marketing ramp. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing display type (rem/clamp, 2.2-6.4rem) stays on its separate ramp, as do four decorative emoji / serif-hero sizes. Verified on a running local stack: pinned sidebar rows and group labels measure 12px/16px, nav items 14px/20px — identical to pre-migration. An audit of every rendered font size across the product surfaces finds nothing off the scale; the only exceptions are avatar initials and emoji, which actor-avatar.tsx sizes proportionally to the avatar diameter by design. Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
da7451843b |
MUL-5494: feat(transcript): render tool events as diffs, content and terminal output (#6134)
* feat(transcript): make tool events readable — diffs, content, terminal output The expanded transcript row printed a tool call's input as raw JSON, so an edit showed `old_string`/`new_string` as escaped one-line literals — the one event type where seeing *what changed* matters most. Tool results kept their JSON string encoding, so every shell result read as a quoted blob with literal `\n`, in the collapsed summary and in Copy all as well as in the body. What each kind of event now renders as: - A replacement reads as a diff. Unchanged runs fold to `⋯` with three lines of context either side, so a one-line change inside a large `old_string` is not buried in text that never moved. - A whole-file write reads as plain content with a line count. There is no before side to compare against, so a `+` on all of it carries no information. - A result is unwrapped once, everywhere it appears. File mutations are identified by the *shape* of the input (`file_path` plus `old_string`/`new_string`, or `content`), never by tool name: the presenter's contract is to keep provider-native names verbatim, and those differ per provider. The write mode keys on `content` rather than on "the before side is empty", because an edit with an empty old_string is an insertion into a file that already exists and still reads as a diff. Highlighting reuses the rich-content engine (`lowlight` and the `.hljs-*` class contract), so a file looks the same in a transcript as it does in a comment, with no new dependency. Each side is highlighted as ONE block and then split at newlines, re-opening the enclosing spans per line — highlighting line by line would break every multi-line string, comment and template literal. Grammar comes from the file extension; an unknown extension stays plain rather than guessing. The hljs palette was scoped to `.rich-text-editor`; it now also covers `.transcript-code`, with no colour definition duplicated. Diffing is a small LCS over lines, degrading to a plain replacement block past 250k cells. Line numbers are deliberately absent: the transcript stores only the tool input, so a snippet's position inside its file is not knowable here, and relative numbers would read as file lines and mislead. * fix(transcript): keep the show-all label off the line it covers The fade overlay does not fully clear the clipped line, so the transparent "Show all" label rendered on top of whatever text sat behind it — the two interleaved character by character and neither was readable. Giving the button an opaque surface separates them. Pre-existing: any tool output long enough to clip hit it. It became routine once whole-file writes started rendering their content. |
||
|
|
545afea827 |
Revert "feat(ui): establish a role-named type scale and migrate ad-hoc font s…" (#6116)
This reverts commit
|
||
|
|
d68d636c91 |
feat(ui): establish a role-named type scale and migrate ad-hoc font sizes (MUL-5451) (#6108)
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 of them written as arbitrary text-[Npx] values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the 1,900-call-site rename (text-sm -> text-body and friends) moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: - 8 / 9 / 10px -> micro (11px): 102 sites, mostly badges and overline labels. Deliberate — under 12px is a counter role, not a text role. - 17px -> title (18px), 22px -> display-sm (24px), 30px (text-3xl) -> display (36px): 21 sites, all headings or stat numbers in flexible containers. - Half-pixel steps are gone entirely. Arbitrary sizes also inherited whatever line-height was above them; the tokens now pin one, which removes latent overflow risk in the fixed-height h-4/h-5 badges those sizes were used in. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing-page display type (rem/clamp, 2.2-6.4rem) is marketing typography on a separate ramp and stays out of the scale, as do four decorative emoji / serif-hero sizes. A guard test fails the build on any font size written outside the scale, reporting file:line — without it nothing in a Tailwind build makes an off-scale value look wrong, which is how the drift happened. Verified against a local stack: typecheck, lint and the full TS suite show no new failures, and before/after screenshots of issues, issue detail, runtimes (populated), usage, agents, inbox, my-issues and settings differ only in the intended 10px -> 11px labels, with no row-height, truncation or layout shift. Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9ff766d700 |
fix(ui): tabular figures on live task timers (MUL-5448) (#6094)
The 1Hz elapsed counters rendered with proportional figures, so the text width changed on 9s -> 10s and 1m 9s -> 1m 10s, nudging the neighbouring elements once a second for the whole run. Web/desktop take the `tabular-nums` utility. Mobile cannot: Tailwind compiles that class to `font-variant-numeric`, and react-native-css-interop's property allow-list only carries `font-variant-caps`, so the declaration is dropped and the class is a silent no-op on RN. Use RN's `fontVariant` style prop there instead. Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
67d3952ee2 |
fix(transcript): auto-follow live task output in transcript dialog (#5932)
* fix(transcript): auto-follow live task output in transcript dialog (#5921) Since the transcript event list moved to Virtuoso (#5733), a live task's new events required manual scrolling to see — the dialog opened at the top and never followed appended output. Wire live-follow through Virtuoso's own primitives, per sort direction: - Chronological (default): `followOutput` returns "smooth" while the task is live and the reader is at the bottom (Virtuoso's own atBottom tracking, with a forgiving 120px threshold). Scrolling up suspends the follow until the reader returns. - Newest-first: growth is PREPENDS, which the firstItemIndex anchoring deliberately holds in place — so a reader parked at the top would silently stop seeing new rows. Track the top edge via `atTopStateChange` into a ref and snap back to index 0 when new events arrive while at the top. Readers who scrolled away are left in place. - Opening a live chronological transcript now lands on the newest event (`initialTopMostItemIndex: LAST/end`, same pattern as the chat list); the per-listEpoch remount re-applies it after task/sort/filter changes. Completed tasks keep opening at the top. Fixes #5921. * fix(transcript): rework newest-first live follow as a user-intent latch (#5921) The previous approach gated the newest-first follow on "is the viewport at the top right now" — but firstItemIndex prepend anchoring moves the viewport away from the top on every flush, so the signal broke itself: after the first prepend the follow silently disengaged and never recovered. Replace it with a pure latch controller (transcript-follow.ts): - Disengage only on accumulated USER displacement (wheel/touch/key deltas, scrollbar drag) beyond the 120px edge zone; system displacement never counts, no matter how far it pushes the viewport. - While following, non-user displacement is pinned back to the live end on the scroll event itself (Virtuoso's prepend compensation lands after React effects, so an effect-timed snap alone stays one flush behind). - Enforcement is suppressed while the mouse is held (text-selection autoscroll) or mid-gesture; returning within the edge zone re-engages. - Timeline segment clicks explicitly unlatch so navigation isn't pinned back. Chronological followOutput switches "smooth" -> "auto": a still-animating smooth scroll reads as "not at bottom" on the next flush and drops the follow under rapid appends. The latch decision table is unit-tested (transcript-follow.test.ts), and the mechanism was validated end-to-end against react-virtuoso 4.18.7 in a browser harness: follow-at-top, in-zone nudge + flush (no false disengage), scroll-away with stable anchor, return-to-top re-engage, rapid flushes. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Naiyuan Qing <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
00e658401b |
feat(views): revamp execution log — virtualized, readable, two-tier header (#5890)
Combines the transcript work (previously split across #5860 virtual scroll and #5871 reading hierarchy) into one branch on current main, folding in the runtime-alias display from #5881. - Virtualize the event list (react-virtuoso) so a multi-thousand-event run mounts a bounded number of DOM rows (#5733), with firstItemIndex anchoring for newest-first live prepends. - Reading hierarchy via a pure trace-event-presenter: agent text and errors render expanded in place through RichContent (compact, log-scale markdown); thinking/tool rows fold to one line; tool detail expands into a quiet surface with "show all". - Persisted 3-way expand density (smart/expanded/collapsed) with per-row overrides; legacy defaultExpanded boolean migrated. Filters always persist (dropped the preserve-filters toggle). - Two-tier header: identity row (status, agent, trigger source, triggered-by) + list toolbar (created/duration/events facts left, shared Button controls right); runtime/provider/workdir/timestamps move to an ⓘ popover, runtime shown via runtimeDisplayName (#5881). - Copy-all exports full event bodies (redacted) with RFC 3339 timestamps (#5873), not the truncated summary. Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
7cc763bbfb |
fix(runtimes): show runtime aliases across user-facing surfaces (MUL-5248) (#5881)
Several UIs rendered the raw daemon name (runtime.name) and dropped the user's custom_name alias: the Skills "Copy from runtime" selector, the Skills list/detail source, the agent creation runtime chip, the agent runtime filter, the agent overview, the skills/MCP runtime hints, the runtime delete confirmations, the desktop runtime window title, the onboarding runtime cards/hints, and the task transcript chip. Route every user-visible runtime label through the shared display contract: - runtimeDisplayLabel (alias + provider) for standalone labels - runtimeDisplayName (alias only) where a provider icon/text sits beside it - machine.title + runtimeRowLabel(runtime, machine.title) inside machine groups The Skills "Copy from runtime" selector is now machine-grouped via the shared buildRuntimeMachines/runtimeRowLabel helpers instead of a flat raw-name list; runtime.name stays the source of identity for hostname parsing, grouping, search, and protocol payloads only. Add a conventions rule (en + zh) forbidding raw runtime.name in user-visible JSX/i18n/Select labels/document titles. Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a767c5b361 |
feat(transcript): include timestamps in copied events (#5873)
Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
310634064f |
fix(transcript): explain unavailable Antigravity live events (#5182)
* docs: design Antigravity live transcript empty state * docs: plan Antigravity live transcript empty state * fix(transcript): explain unavailable Antigravity live events * chore: remove agent planning artifacts --------- Co-authored-by: CAVIN <[email protected]> |
||
|
|
c599f47ba4 |
fix(redact): cover GitHub fine-grained PATs and Google API keys (#4678)
The primary server redactor now covers github_pat_ and standard AIza keys, but the client transcript safety net does not. Add matching client patterns and regression coverage so previously stored or otherwise unredacted transcript values are masked at display time. Also make the fixed-length Google key rule redact a key ending in '-' while preserving its trailing delimiter. Keep the 39-character format exact to avoid broadening false positives. Co-authored-by: multica-agent <[email protected]> |
||
|
|
4fac8d772f |
feat(attribution): Human Attribution Phase 1 (MUL-4302) (#5150)
* feat(attribution): Phase 1 foundation — provenance schema + resolver (MUL-4302)
Human Attribution, Phase 1 (地基) first increment. Every agent run must be
traceable to exactly one accountable human AND record at which waterfall level
that human was resolved, so a NULL originator can be told apart from a genuine
'no human in the chain'.
- migration 149: add originator_source (waterfall label) + delegation/retry/
rerun/rule-version lineage + kind-tagged trigger evidence to agent_task_queue.
No FK, no cascade, no CHECK on the source enum (MUL-4302 §7); nullable ADD
COLUMNs = fast metadata-only change on the hot queue table.
- internal/attribution: the accountable-human vocabulary (Source, EvidenceKind,
TriggerKind) + pure, unit-tested classification rules (ClassifyComment/
ClassifyDirect). No DB, no authorization — provenance labeling only.
- service: attributionFor{IssueTask,TriggerComment} gather facts and delegate to
the pure classifier; the legacy originator resolvers now delegate here so
there is one source of truth. originator_user_id's VALUE is unchanged, so the
Composio-overlay and canInvokeAgent A2A authorization boundaries are
byte-for-byte preserved (MUL-4302 §1.3).
- enqueueIssueTask / enqueueMentionTask stamp originator_source + evidence;
CreateRetryTask carries the parent attribution forward and records
retry_of_task_id so retry and manual rerun stay separable (MUL-4302 §5).
Verified: go build ./..., go vet, gofmt clean; new attribution unit tests +
enqueue stamping integration test green; existing resolve_originator tests
unchanged.
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): split accountable_user_id from originator, close enqueue bypasses (MUL-4302)
Phase 1, per Bohan's decision on the MUL-4302 thread: audit and authorization
answer different questions and get different columns.
- Migration 151 adds agent_task_queue.accountable_user_id (no FK, no cascade).
Authorization keeps reading ONLY originator_user_id (canInvokeAgent A2A gate,
Composio overlay); audit/UI/usage read accountable_user_id + source + evidence.
- Invariant (finalizeAttribution, single chokepoint + §11 tests): originator
non-null ⟹ accountable equals it. The two diverge only when originator is null
(autopilot / degraded fallback), which is the deferred rule_owner/owner_fallback
increment; this lands the column + mirror-write so that split has a home.
- Close the NULL-source enqueue bypasses Elon flagged: chat, quick-create,
deferred-fallback and run_only-autopilot now stamp originator_source + evidence
(+ accountable where a human exists). Autopilot stays unattributed until the
rule-version snapshot table lands, but is no longer a silent NULL-source row.
Retry inherits accountable_user_id like the rest of the attribution lineage.
- Fix assign/promote attribution (§4): a member who assigns/promotes an existing
issue is now the accountable human (and, by the invariant, originator) ahead of
the issue creator. Threaded as an OPTIONAL actor override, so comment/rerun/
autopilot paths keep today's resolution and create-with-assignee (creator ==
actor) is unchanged. The squad leader gate already judged the same member.
Also merges origin/main: renumbers the attribution migration 149→150 (main took
149 for issue_origin_agent_create) and folds agent_create into ClassifyDirect's
origin inheritance.
go build/vet/gofmt clean; attribution unit tests + service stamp/actor tests +
handler suite pass on a fresh DB migrated through 151.
Co-authored-by: multica-agent <[email protected]>
* docs(attribution): fix accountable NULL semantics + close chat/quick-create evidence boundary (MUL-4302)
Addresses Elon's 2nd-round review on PR #5150 (pre-merge doc/evidence items):
- Migration 151 no longer overclaims NULL. accountable_user_id is NULL not only
on pre-migration rows but on NEW rows whose audit source resolved no human yet
(run_only autopilot writes originator_source='unattributed' with NULL
accountable until rule_owner lands). Header + COMMENT ON COLUMN reworded so a
schema reader does not misjudge the invariant.
- Chat now uses the UNIFORM evidence pair (kind=chat, ref=chat_session_id), like
autopilot_run/issue_assignment, instead of relying only on the dedicated
chat_session_id column — new EvidenceChat kind. Added a service test asserting
chat stamps direct_human + chat evidence.
- Quick-create is documented as the ONE intentional no-antecedent-row path: no
comment/issue/session/run exists at enqueue time (the run creates the issue), so
trigger_evidence_kind/ref stay NULL while the human rides originator/accountable
and source is direct_human — not a NULL-source bypass.
No authorization behavior change. attribution + service + handler suites pass on a
DB migrated through 151.
Co-authored-by: multica-agent <[email protected]>
* chore(attribution): renumber migrations 150/151 → 157/158 after merging main (MUL-4302)
main's #5162 ("unblock release migrations") renumbered the chat migrations and
took 150 (agent_task_coalesced_comments) and 151 (chat_read_cursor), colliding
with this branch's attribution migrations. Renumber them above main's new highest
(156) so TestMigrationNumericPrefixesStayUniqueAfterLegacySet passes:
- 150_agent_task_attribution → 157_agent_task_attribution
- 151_agent_task_accountable_user → 158_agent_task_accountable_user
Fixed the internal "migration 150" references in 158's header to 157. Migrations
apply cleanly through 158 on a fresh DB; migration lint green.
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): autopilot rule_owner — accountable = rule version publisher (MUL-4302)
Implements rule_owner (MUL-4302 §3.4), the first attribution source where the
accountable human diverges from the (NULL) authorization originator.
- Migration 159 adds the append-only autopilot_rule_version snapshot table (no FK,
no cascade); migration 160 adds its CONCURRENTLY lookup index.
- Write-on-publish: CreateAutopilot appends v1 (publisher = creator); UpdateAutopilot
appends a new version when a SUBSTANTIVE autopilot-row field changes (assignee /
status / execution_mode) — cosmetic edits (title/description/template) write none.
Both run inside the existing handler tx (atomic with the autopilot write).
- Dispatch resolution: both autopilot execution modes now resolve the active rule
version and stamp originator_source='rule_owner', accountable_user_id=publisher,
rule_version_id=<snapshot>, with originator_user_id left NULL (authorization
unchanged). run_only stamps CreateAutopilotTask directly; create_issue resolves in
attributionForIssueTask so both modes attribute identically. A missing version /
non-member publisher degrades to unattributed — never fabricates a human.
- finalizeAttribution now enforces the invariant ONE-WAY: it mirrors originator onto
accountable only when originator is valid, leaving an explicitly-set accountable
(rule_owner / future owner_fallback) intact when originator is NULL. Added
rule_version_id to CreateAgentTask so the create_issue path persists it too.
Also merges origin/main and renumbers this branch's attribution migrations
150/151 → 157/158 (main's #5162 took 150/151); rule_version table is 159/160.
Tests: attribution unit RuleOwner + one-way invariant table; service integration
tests proving an autopilot-origin issue stamps rule_owner + rule_version_id (and
degrades to unattributed with no version). Full service/attribution/handler/
migration suites pass on a DB migrated through 160; build/vet/gofmt clean.
Deferred (same PR): trigger-table republish (cron/webhook/event_filters) and
system-pause/archive versioning; owner_fallback + fail-closed; manual rerun.
Co-authored-by: multica-agent <[email protected]>
* fix(attribution): manual autopilot trigger → direct_human to the triggering member (MUL-4302)
Elon's blocking finding: a member manually triggering an autopilot was attributed
rule_owner (accountable = rule publisher, originator NULL) like a schedule/webhook
run, so member B triggering member A's autopilot landed accountable=A and carried
no originator authorization context for the run. Per MUL-4302 §4 a manual "run now"
is a direct human action and must attribute direct_human to the triggering member.
- Thread the triggering member from TriggerAutopilot into dispatch: new
DispatchAutopilotManual carries actorUserID (resolved via resolveActor +
memberActorUserID, so only a member actor is a human; an A2A agent actor falls
back to rule_owner). DispatchAutopilot / DispatchAutopilotForPlan keep their
public signatures (pass an invalid actor); only the internal dispatchAutopilot /
dispatchCreateIssue / dispatchRunOnly gained the param, so the many existing
callers are untouched.
- run_only: dispatchRunOnly stamps direct_human (originator == accountable ==
actor, no rule_version) for a manual actor, else rule_owner. CreateAutopilotTask
gains an originator_user_id param for the manual case.
- create_issue: dispatchCreateIssue enqueues a manual trigger via the actor-carrying
*WithHandoff entry points; attributionForIssueTask's autopilot-origin rule_owner
branch is now guarded on !actorUserID.Valid, so a valid actor falls through to the
direct_human override. Both execution modes attribute identically.
- schedule / webhook keep rule_owner (no actor). Trigger-table + system-pause/archive
versioning remain the pre-merge follow-ups.
Tests: the run_only row assertion Elon asked for (schedule → rule_owner row on
CreateAutopilotTask), plus manual direct_human on BOTH modes (run_only and
create_issue), including a manual actor distinct from the rule publisher. Full
service/attribution/handler/migration/scheduler/cmd suites pass on a DB migrated
through 160; build/vet/gofmt clean.
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): owner_fallback + fail-closed policy, and manual-rerun direct_human (MUL-4302)
Two of the three remaining Phase 1 items (trigger-table / system-pause versioning
is deferred — see PR description).
owner_fallback + fail-closed (§1/§3.5) — the never-null accountable guarantee:
- attribution.OwnerFallback degrades an UNATTRIBUTED result to owner_fallback:
accountable = agent owner, originator stays NULL (audit-only, authz untouched),
Source.Precise()==false. finalizeAttribution's one-way invariant already allows
accountable-set / originator-NULL divergence, so nothing else changes.
- Migration 161 adds workspace.attribution_fail_closed (default FALSE) + a lean
GetWorkspaceAttributionFailClosed read. (Also added the column to ListWorkspaces'
explicit column list so its row type stays db.Workspace.)
- applyAttributionFallback is applied at every enqueue boundary (issue, mention,
chat, quick-create, deferred-fallback, autopilot run_only): unattributed →
owner_fallback (agent owner) by default, or ErrAttributionFailClosed when the
workspace is fail-closed, which the caller surfaces to refuse the enqueue (the
run does not start). So no run is left without an accountable human, and a
compliance workspace can block unattributable runs instead.
manual rerun (§5) — a rerun is a NEW direct_human trigger to the rerunning member:
- RerunIssue threads the acting member (resolved in the handler via resolveActor)
down to enqueueRerunTask, and attributionForIssueTask is now actor-first so the
actor wins over an INHERITED trigger comment (a rerun keeps the comment for the
daemon's prompt context but must attribute to whoever clicked rerun, not the
original comment's human).
- rerun_of_task_id lineage is recorded via a targeted SetAgentTaskRerunOf update on
the rerun path only (keeping the shared CreateAgentTask insert untouched), so
system retry (retry_of_task_id) and human rerun stay separable in reporting.
Tests: OwnerFallback unit test; owner_fallback + fail-closed-refusal + manual-rerun
(direct_human + rerun_of_task_id) service tests; the prior "degrades to unattributed"
test updated to owner_fallback. Full service/attribution/handler/migration/scheduler/
cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <[email protected]>
* fix(attribution): close fail-open holes + move rerun_of_task_id into creation snapshot (MUL-4302)
Addresses Elon's two must-fixes on PR #5150.
1. accountable-never-null / fail-closed had fail-open holes. applyAttributionFallback
now, for an UNATTRIBUTED run, refuses the enqueue (ErrAttributionFailClosed) in
THREE cases instead of silently degrading to a runnable NULL-accountable task:
- workspace policy read fails (or no workspace) → fail closed; we cannot confirm
fallback is permitted, so we don't run an unattributable task on a DB hiccup.
(Only the rare unattributed path pays this; precise runs never read the policy.)
- workspace is fail-closed → refuse (unchanged).
- owner_fallback has no valid agent owner → refuse rather than enqueue a task with
a NULL accountable_user_id.
ErrAttributionFailClosed's doc now covers all three "cannot guarantee an
accountable human" refusals. Added missing-owner / policy-read-failure /
precise-passthrough tests.
2. manual rerun rerun_of_task_id was a post-notify UPDATE (race: the queued event /
daemon claim could see rerun_of_task_id = NULL, and a failed update degraded the
run to a plain direct_human). It now rides the CreateAgentTask insert — threaded
through enqueueIssueTask / enqueueMentionTask as a creation param (like
retry_of_task_id) so it is written in the same statement before the daemon is
notified. Removed the SetAgentTaskRerunOf follow-up query.
Also merges origin/main (unrelated CLI fix #5167, no conflict). Full service /
attribution / handler / migration / scheduler / cmd suites pass on a DB migrated
through 161; build / vet / gofmt clean.
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): rule_owner versioning on trigger edits + system-pause/archive (MUL-4302)
The final remaining Phase 1 item: substantive publishes beyond the autopilot row now
republish the rule version, so a run's rule_owner accountable follows whoever last
changed what the rule does.
- Extracted the config-summary + insert into service.RecordAutopilotRuleVersion so
the handler and the (different-package) failure monitor share one writer; the
handler's recordAutopilotRuleVersion is now a thin wrapper.
- Trigger edits: UpdateAutopilotTrigger and DeleteAutopilotTrigger republish the rule
version with the acting member as publisher, ATOMICALLY (tx-wrapped mutation +
version write, mirroring CreateAutopilot/UpdateAutopilot). CreateAutopilotTrigger
republishes best-effort — the webhook path mints its token with a retry loop that
cannot share one tx, and a create is usually initial setup already covered by v1;
a failed write there is benign (active version stays the current publisher, the new
trigger fires under it, no immediate daemon claim rides it).
- Archive (DeleteAutopilot) republishes (member, status=archived), tx-wrapped.
- System auto-pause (failure monitor) republishes with a 'system' publisher,
best-effort — a background sweep to a non-dispatching state (a paused autopilot
never dispatches; a later member resume supersedes).
- RotateWebhookToken / SetSigningSecret deliberately do NOT version: they rotate
credentials, not the rule's behavior (not §3.4 substantive).
Semantics: a system-published (no-member) active version degrades dispatch to
unattributed → owner_fallback, never fabricating a human.
Tests: republish-reattributes (member A → member B supersedes → dispatch resolves to
B; system publisher → unattributed). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Also merges origin/main (unrelated frontend feature #5074).
Co-authored-by: multica-agent <[email protected]>
* fix(attribution): make trigger-create rule-version republish atomic (MUL-4302)
Addresses Elon's final Phase 1 blocking finding: CreateAutopilotTrigger recorded the
rule-version republish best-effort AFTER the trigger insert. If member B added a
schedule/webhook trigger to member A's autopilot and the version write failed, future
schedule/webhook dispatches would keep attributing to A — violating the rule_owner
invariant that the last member to substantively change the rule owns future runs
("no immediate daemon claim" doesn't save it, since the miss surfaces at the LATER
trigger firing).
Both create paths now write the version in the SAME tx as the trigger INSERT:
- schedule create: wrap CreateAutopilotTrigger + recordAutopilotRuleVersion in one tx.
- webhook create: each mint-with-retry attempt runs in its own tx (insert + version
commit together; a token collision rolls that attempt back and retries with a fresh
token; a version-write failure rolls the trigger back). Passes ap + the acting
member id into the helper.
- removed the best-effort recordTriggerRuleVersionBestEffort helper (and the now-unused
slog import).
Test: TestCreateTrigger_RepublishesRuleVersionAtomically drives both create paths
through the handler and asserts a rule version is published by the acting member.
Existing webhook/trigger/archive handler tests still pass. Also merges origin/main
(unrelated avatar feature #5074). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): Phase 2.1 — surface run attribution on the task API (MUL-4302 §9)
First Phase 2 (visibility) increment: the agent-task API now returns the resolved
accountable-human provenance so the UI can render an "on behalf of" badge.
- AgentTaskResponse gains an `attribution` object: source label (never blank —
pre-migration NULL renders "unattributed") + `precise` flag (false for the degraded
owner_fallback / backfill / unattributed sources), the initiator (accountable) and
originator (authorization) user refs, the evidence {kind, ref_id} pointer, and the
rule_version / delegated / retry / rerun lineage ids.
- The label + evidence + raw ids are built in the PURE taskToResponse (no DB), so
every task response carries them. Names are hydrated separately, only on the
user-facing surfaces (ListAgentTasks, ListWorkspaceAgentTaskSnapshot, RerunIssue,
CancelTaskByUser) — daemon-claim paths stay lean.
- Hydration resolves initiator/originator from the GLOBAL user table (departed-member
safe) via a new batch GetUsersByIDs query (no N+1); best-effort, so a lookup hiccup
leaves the raw ids intact.
Tests: pure taskAttributionBase (direct_human / rule_owner NULL-originator /
owner_fallback degraded / pre-migration→unattributed) + DB hydration (fills known
ref, leaves unknown id un-filled, skips nil). Full handler/service/attribution/
migration/scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt
clean. The field is additive — the frontend's parseWithFallback ignores unknown keys,
so nothing breaks until the UI increment consumes it.
Also merges origin/main (unrelated editor feature #5090).
Remaining Phase 2 (next increments, same PR): frontend zod schema + "on behalf of"
badge + evidence-chain jump; append-only correction events (write + display).
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): Phase 2.2 — on-behalf-of badge in the execution log (MUL-4302 §9)
Surface the accountable human on every agent run row:
- AttributionBadge composes Badge + ActorAvatar, shows "on behalf of <member>"
with the resolution source as a tooltip; degraded (non-precise) attribution
gets a warning tone, and an unresolved initiator renders an explicit
"no responsible member" chip.
- Wire the badge into both active and past rows of the execution log.
- Mirror the attribution shape into AgentTaskResponseSchema (defensive, .loose())
so the cancel-task path carries it through zod; add parse tests.
- Export TaskAttribution/AttributionUser/TaskEvidence from @multica/core/types
and add the attribution block to all four issues.json locales.
Co-authored-by: multica-agent <[email protected]>
* fix(attribution): hydrate initiator names on issue-facing task endpoints + bound the badge (MUL-4302 §9)
Address Elon's PR #5150 review:
- ListTasksByIssue (the execution-log data source), GetActiveTaskForIssue and the
issue-scoped CancelTask now call hydrateTaskAttributions, so the "on behalf of
<member>" badge shows the real member name on issue detail instead of falling
back to "someone". Mirrors the existing ListAgentTasks / snapshot behavior.
- AttributionBadge: cap width (max-w-40, min-w-0) and truncate the name span so a
long name / narrow right column can't squeeze out trigger/status/actions; keep
the avatar shrink-0.
- Add a handler test asserting the issue task list returns a hydrated
attribution.initiator.name.
Co-authored-by: multica-agent <[email protected]>
* fix(attribution): use semantic AvatarSize 'xs' for the badge avatar
main refactored ActorAvatar.size from a raw pixel number to the semantic
AvatarSize union (packages/ui/lib/avatar-size). Switch the on-behalf-of badge
avatar from size={14} to size="xs" (16px) after merging main.
Co-authored-by: multica-agent <[email protected]>
* fix(attribution): stage-cascade falls back to parent-issue provenance, not agent owner (MUL-4302)
When closing the last sub-issue in a Stage wakes the parent's assignee agent, the
run was enqueued via a system-authored child-done comment with no actor, which the
resolver classified as unattributed and then degraded to owner_fallback (the agent's
own owner). That is the wrong accountable human: the woken run should be accountable
to whoever caused the parent issue to exist.
attributionForIssueTask now detects a system-authored trigger comment and falls
through to the parent issue's own provenance — the same creator / agent_create-origin
/ autopilot-origin chain a direct enqueue resolves (so an agent-decomposed parent
attributes via delegation to the human who drove it; a member-created parent to that
member; an autopilot parent to the rule publisher). owner_fallback is now only the
last resort when the parent provenance itself has no human.
- Extract attributionFromComment so attributionForIssueTask can inspect author_type
without a second GetComment; authorization resolution stays byte-identical.
- Add a DB-backed test asserting a system child-done comment resolves to the parent
issue's origin human (delegation), not owner_fallback.
Co-authored-by: multica-agent <[email protected]>
* feat(attribution): autopilot runs attribute to the firing trigger's creator (MUL-4302)
Per Bohan: an autopilot schedule/webhook run should be accountable to the human
who created the SPECIFIC trigger that fired it, not the rule publisher. (Manual
triggers already attribute to the invoking member via direct_human — unchanged.)
- Migration 162: add autopilot_trigger.created_by_type/created_by_id (nullable, no
FK/cascade). Capture the creating member at both trigger-create sites (schedule +
webhook).
- New precise source trigger_owner: originator stays NULL (an autonomous fire
carries no human authorization — same authz-safe divergence as rule_owner),
accountable = the trigger's member creator.
- triggerOwnerAttribution resolves run.trigger_id → creator; wired into run_only
dispatch and the create_issue path (bridging issue → active run → trigger_id).
Legacy triggers with no recorded creator, and agent-created triggers, degrade to
rule_owner then owner_fallback — nothing regresses.
- Frontend: trigger_owner source label in all four locales + badge switch case.
- Tests: attribution TriggerOwner unit + Precise/invariant; DB-backed resolver
tests (member creator → trigger_owner; creatorless → rule_owner fallback).
Co-authored-by: multica-agent <[email protected]>
* chore(attribution): re-trigger CI (dropped synchronize event on
|
||
|
|
f4de0948a2 |
refactor(ui): unify ActorAvatar size tiers + round all avatars & cropper (MUL-4277, MUL-4184) (#5133)
* refactor(ui): converge ActorAvatar size to semantic tiers (MUL-4277) Replace the free-form numeric `size` on ActorAvatar with a constrained `AvatarSize` union (xs/sm/md/lg/xl/2xl) so avatar dimensions are chosen by role instead of ad-hoc pixels. This eliminates the magic-number drift where the same role rendered at different sizes across pages. - Add `@multica/ui/lib/avatar-size` (AvatarSize union + AVATAR_SIZE_PX map + default tier). - Base `ActorAvatar` (packages/ui) and business `ActorAvatar`/`AgentStatusDot` (packages/views) now take `AvatarSize`; internal font/icon math and the presence-dot threshold read px from the map. - Migrate all web/desktop call sites (packages/ui + packages/views) from numeric sizes to tiers using the role table (12,14->xs 16,18,20->sm 22,24,28->md 30,32,34->lg 40,44->xl 56,64->2xl). - Token-ise the derived consumers `AgentAvatarStack` and `IssueAgentActivityIndicator` (px looked up internally for overlap/+N math). Out of scope (per plan): ui/avatar.tsx primitive, account-tab/AvatarPicker, mobile, and component-name disambiguation. Co-Authored-By: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(ui): unify all avatars and the upload cropper to round (MUL-4277, MUL-4184) main's avatar-shape decision rendered non-human actors (agent, squad, system) and the workspace logo as rounded squares, and the upload cropper mirrored that with a square crop window. Per the updated decision (avatars_and_cropper_round_required), every avatar and the crop UI are now circular; the square path is removed rather than left as dead config. - Base ActorAvatar: always rounded-full (drop the isHuman/rounded-md split). - avatar-crop-dialog: remove the AvatarCropShape/square path; crop window is always cropShape="round". - avatar-upload-control: drop VARIANT_SHAPE; the control is always round and no longer threads a shape to the dialog (variant still drives the fallback). - Strip rounded-md/rounded-none square overrides from agent/squad/member ActorAvatar call sites; round the read-only agent/squad static wrappers. - WorkspaceAvatar: round the org logo so it matches the (now round) workspace upload/crop and the shared avatar shape. Co-Authored-By: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(ui): make round avatar shape a hard invariant (MUL-4277) Close the two remaining square squad-avatar paths flagged in review and prevent call sites from re-squaring the avatar: - base ActorAvatar: keep `rounded-full` as the last class in cn() so a call-site `className` can no longer override the circle. - SquadHeaderAvatar: drop `className="rounded"` (was overriding the base circle into a small rounded square). - SquadsPage no-avatar fallback: route through the shared ActorAvatarBase (`isSquad size="lg"`) instead of a hand-written rounded-md tile, so the fallback matches the image path — one shape source of truth. Co-Authored-By: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(views): round the agent/squad avatar loading skeletons (MUL-4277) The avatar placeholder skeletons on the agent/squad list, detail, and profile-card loading states were still rounded squares (rounded-md/lg) from the pre-round era, so the avatar visibly popped from square to circle on load — inconsistent with the round avatars and with the member/inbox/issue skeletons that already use rounded-full. Round all five: agents-page, agent-detail-page, squads-page, squad-detail-page, squad-profile-card. Co-Authored-By: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
dd9996d0aa |
MUL-4014: persist transcript filters and expansion (#4884)
* feat(transcript): persist log view preferences Co-authored-by: multica-agent <[email protected]> * fix(transcript): wrap modal header controls Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
2857a4c649 |
fix(transcript): live-update issue/agent transcript dialog from shared cache (#4452)
The transcript dialog opened from a running task's row showed only a one-shot snapshot taken at open time: TranscriptButton fetched once via api.listTaskMessages and cached it locally, never subscribing to the shared ["task-messages", taskId] cache that the WS task:message stream already seeds. New tool calls / thinking / text never appeared until the task finished or the page reloaded. Add a live-cache mode to the shared TranscriptButton: when isLive and the parent provides no items and the task id is a persisted UUID, render from the shared task-messages cache so the open dialog grows in real time. On open (and again on the running→terminal transition) force a backfill via api.listTaskMessages and merge it into the cache by seq — taskMessagesOptions is staleTime:Infinity, so a plain subscription never heals a WS reconnect gap. The cache observer is read-only (enabled:false) so React Query never blind-replaces the cache; only the WS handler and the seq-merged backfill write it. The subscription mounts only while the dialog is open, so closed live rows add no baseline requests; terminal tasks keep the lazy one-shot fetch. Covers issue execution-log and agent activity. Autopilot issue-less run_only live log is out of scope: the backend doesn't broadcast task:message for tasks with no issue/chat session, so there's nothing to subscribe to — backend broadcast unchanged. Extract mergeTaskMessagesBySeq into packages/core/chat/queries.ts and route both the realtime task:message handler and the new backfill through it, so there is one seq-merge semantics for that cache instead of two. Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: multica-agent <[email protected]> |