Before: zai-beta carried the platform admin console and open-connector
work (#7, #8, #9) on top of main.
After: it also carries the project feed view, the deliverable focus view,
the project sidebar panels and the issue-detail performance work from
PR #6, so the zai-beta images include every integration branch feature.
How: merge with two append-only conflict resolutions
(concurrentIndexCleanups in server/cmd/migrate/main.go and the schema
exports in packages/core/api/schemas.ts); migrations 585-587 do not
collide with 590-606. CHANGELOG-zai.md gains the PR #6 section.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: a project had no way to list the files its issues carry, and no
issue to attach a file uploaded straight to the project.
After: GET /api/projects/{id}/attachments lists the attachments of every
issue in the project, newest first, paged, filterable by source
(deliverable = agent upload, upload = member upload), with each row's
issue {id, identifier, title}, the total, and both group totals. POST
/api/projects/{id}/library-issue returns the project's "Project files"
issue {id, identifier}, creating it on first use as a top-level,
unassigned backlog issue.
How: one sqlc join attachment -> issue on attachment.issue_id (comment
attachments keep their issue_id, so each row appears once), excluding
tombstoned comments and source-context snapshot copies; existing
idx_issue_project_status and idx_attachment_issue cover the join, so no
index is added. The category filter stays client-side because the
frontend buckets files with preview-kind and extension tables that
cannot be mirrored here without a second copy. The holder issue is
created through IssueService.Create, whose per-(project, title)
duplicate lock makes concurrent first uploads converge on one issue.
DB-backed tests are written and compile-checked but not run here.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: an agent that built several HTML pages for a prototype could only
hand them over as separate files, and a reader had to open and page through
each one to see how the screens relate.
After: a `canvas.json` (or `*.canvas.json`) deliverable opens in the
full-screen focus as an infinite pan / zoom canvas of the issue's HTML
deliverables. Each frame shows the latest version of its HTML file in a
sandboxed iframe at its x / y / w / h, labelled with its name. Wheel or pinch
zooms around the cursor, dragging the background (or space + drag) pans, and
"Fit all" fits the page. A "Prototypes" list centers a frame, and Play opens
the frame's HTML full screen in the same focus, with "Back to canvas" in the
top bar. Frames can be dragged; "Save layout" uploads a new version of the
canvas file and posts it as a `/note` comment carrying the file card, so the
version joins the deliverable and wakes no agent. A new canvas version is
followed once loaded, and only frames whose HTML attachment changed remount.
The agent brief gets one optional paragraph describing the file shape.
How: `@multica/core/attachments/prototype-canvas` holds the pure parts (zod
schema with defaults, `parsePrototypeCanvas`, `moveFrame` on the raw JSON,
`fitAllTransform`, `centerOnFrame`, `zoomAround`, `isCanvasFilename`).
`AttachmentPreviewPanel` takes an optional `stage` that replaces the kind's
body while keeping the top bar and prev / next. The focus renders
`PrototypeCanvas` as that stage, tracks the canvas it was played from, and
follows new versions with a small `useFollowLatestVersion` hook that mirrors
the 4-D rule.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: a project had no way to list the pull requests its issues delivered; the only PR list was per issue, so a project-level view would have to fan out one request per issue and could not page or count them.
After: GET /api/projects/{id}/pull-requests returns the distinct GitHub and self-hosted VCS PRs linked to the project's issues, newest update first, with the issue PR list's fields (checks rollup, mergeable, additions, deletions) plus the project issues each PR is linked to. state=open|merged|closed filters (open covers draft, closed is closed without merging), limit/offset page per PR, and the response carries total and per-group counts.
How: ListProjectPullRequestPage unions both PR tables and de-duplicates links so paging is per PR; ListProjectPullRequests / ListProjectVCSPullRequests then load full rows for the page ids with the same columns as the per-issue queries, so the existing row serializers are reused unchanged (including the page-visit snapshot refresh). Issues are attached from one extra query. CountProjectPullRequestsByState feeds total and counts. No migration: additions/deletions already exist. DB-backed handler tests are written but not run here (no PostgreSQL).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: GET /api/issues/thread-summaries told a client how many replies an issue had and when the last one landed, but not what it said, so a compact list had to open the thread to show it.
After: each summary row carries last_reply_excerpt, the newest counted (non-deleted, non-system) comment flattened to one line of at most 80 runes with mentions collapsed to @Label; null when the issue has no counted comment.
How: ListIssueThreadSummaries joins a lateral lookup of the newest counted comment (body capped at 2000 characters in SQL) onto the existing rollup, so the batch is still one query; the handler flattens it with commentExcerpt. A DB-backed handler test covers newest-wins, deleted and system exclusion, mention flattening and truncation (not run here, no PostgreSQL).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: a long document an agent re-uploaded section by section showed up as
a series of versions; the full-screen view stayed on the version the reader
opened, rendered `<!-- pending -->` lines as nothing, and gave no sign of how
far the writing had got.
After: an outline upload whose unwritten sections are single
`<!-- pending: Title -->` lines renders them as dashed "Pending" placeholders.
While the focus is open on the latest version of a Markdown file, a new
version is followed once its text is loaded, the paragraphs it added carry a
label and a ~2s fading highlight, and the view scrolls to the first of them
if the reader was at the bottom. The header shows "Writing - n/m sections"
while an agent run is active on the issue and "Unfinished - n/m" once none
is. Agents are told the convention next to the `--attachment` guidance.
How: pure helpers in core (parsePendingSections, splitParagraphs,
diffParagraphs, writingProgress) with node tests. AttachmentPreviewPanel
takes an optional renderMarkdown; the focus passes ProgressiveMarkdown, which
splits only around placeholders and changed blocks and otherwise renders a
plain ReadonlyContent. Version text comes from useAttachmentHtmlText, the
preview's own query, and the running state from the workspace agent task
snapshot via selectIssueTasks. No backend change.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: a feed card's thread bar only said how many replies an issue had.
To see whether an agent had opened a PR, whether CI was green, or whether a
report had been uploaded, the reader had to open the issue.
After: GET /api/issues/thread-summaries also returns, per issue, up to 3
linked PRs (newest first, with state, diff size and a passed/failed/pending/
none CI verdict), the number of distinct deliverable files and when the
newest was uploaded. The thread bar ends with one chip per PR (state icon and
CI icon, opens the PR in a new tab) and one deliverables chip (count and age,
opens the issue). The peek shows the same strip under its header when the
issue has any output. Issues with a PR or file but no comment now get a row.
How: three batched, workspace-scoped queries over all requested ids (GitHub
PRs, self-hosted VCS PRs, deliverables); PRs are ranked per issue in SQL and
merged in Go. checks_state reuses rollupToConclusion and
aggregateChecksConclusion, so it agrees with the PR list. Deliverables are
files on live comments, grouped by media type and filename like
collectDeliverableFiles. The response schema defaults the new fields for older
servers and maps unknown checks_state values to none. The summary cache is
refetched on pull_request events and on comments that carry files. DB-backed
handler tests were written but not run (no Postgres here).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: The feed gave no sense of how far an agent had got. Agents could
not post a progress report that the feed understands: `comment add` had no
way to set a comment type, and the run brief told agents not to post
progress at all.
After: Each feed message shows a compact checklist under its thread bar
(done/total, a small bar, the first 5 items, "+N more"), and the issue peek
shows the same block under its title. Finished issues (done or closed
category) fold to a single "done/total" line with a chevron to expand. A
progress report is a `progress_update` comment by an agent whose body is a
Markdown task list; the newest one per issue wins.
How: The thread-summaries endpoint gains `latest_progress`
({comment_id, content, created_at} | null) from one batched DISTINCT ON
query, so there is no new endpoint. Core adds the type, a tolerant zod
schema, `parseProgressChecklist`, and a realtime patch for new reports.
Views add `FeedProgressCard`, mounted from the thread bar and the peek, with
strings in all five locales. `multica issue comment add --type
progress_update` lets agents post a report, and the run brief documents it as
the one exception to the single-comment rule.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Add connector_service_policy (per-service publish switch, tool allowlist,
user allowlist) and gate the open-connector catalog, connect flows and task
overlays on it. Publish open_connector_apps from whether the integration is
wired instead of a feature flag, and drop OPEN_CONNECTOR_API_KEY_SERVICES.
Expose /api/admin/open-connector/* (health probe, service catalog with
policy and connection counts, connections by user, audited enable/disable
and policy writes through runAdminAction).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01G4Tynn8YQnR5koJTSvFC62
Resolves the UpdateAgent conflict in handler/agent.go by keeping both the
connector_service_allowlist write and main's runtime-binding transaction,
renumbers the open-connector migrations to 590-596 because main took 583/584,
and regenerates sqlc.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
Before: a feed card showed reply counts and an unread dot, but an agent
asking the reader to decide something looked like any other reply.
After: GET /api/issues/thread-summaries returns awaiting_me per issue
(up to 3, newest first): unresolved threads whose latest comment is
agent-authored and mentions the caller. FeedView renders one decision
card between the message card and the thread bar with the agent avatar,
the newest excerpt, a "+N" count and a button that opens the peek on that
comment.
How: new sqlc query ListAgentQuestionsForIssues picks the latest
non-deleted, non-system comment of each unresolved thread (recursive
walk from the root, like ListCommentRepliesForRoots) and matches
mention://member/<caller> only on that row. The handler flattens mention
markup to @Label and cuts the excerpt to 140 runes. Peek actions gain an
optional { commentId } that reaches IssueDetail as highlightCommentId.
comment:created keeps the server-owned list, clears it for the caller's
own member comment and refetches when the comment may change it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: the feed could not tell which conversations had moved since the
reader last looked, so every card read the same.
After: opening an issue (page or peek) records a per-user read cursor after
1.5 s, posting a comment records it at once, and feed cards whose newest
counted comment is later than the cursor carry a small dot on the left: a
plain dot when the reader spoke last, a ringed one when someone else did
and the reply is theirs to make.
How: table issue_read_cursor (no primary key in the create, a CONCURRENTLY
built unique index on (user_id, issue_id) attached as the primary key in a
later migration, no foreign keys), queries UpsertIssueReadCursor (monotonic
GREATEST on the database clock), ListIssueReadCursors and the three
cleanup deletes wired into issue deletion, member removal and workspace
teardown. PUT /api/issues/{id}/read returns 204; thread-summaries gains
last_read_at for the caller. Core adds api.markIssueRead, useMarkIssueRead
and patchThreadSummariesLastRead; the comment-created patch keeps the
cursor. Views add getFeedReadState and the dot on FeedMessageCard, the
delayed mark in IssueDetail and the immediate mark on send in
useIssueTimeline.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: the issue detail subscribed to the workspace-wide child-progress map (every parent in the workspace) just to draw nested progress rings on its own sub-issue rows, a large response in big workspaces for a tiny need.
After: the detail asks for progress of exactly the loaded sub-issue ids, so the response is bounded by the visible rows. Board, list and swimlane keep the workspace-wide query unchanged, and the endpoint without parent_ids behaves as before.
How: GET /api/issues/child-progress accepts an optional parent_ids filter (comma-separated uuids, at most 100, malformed or oversized input is a 400) backed by the new ChildIssueProgressForParents query, which stays workspace-scoped. In core, childIssueProgressForParentsOptions sorts, dedupes and chunks ids at 100 and nests its key under issueKeys.childProgress so existing realtime and mutation invalidations still reach it.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: GET /issues/{id}/timeline read every comment and activity of the
issue (up to the hard cap) in one response, and the paging params only
changed the response wrapper. The issue page fetched, parsed and held the
whole timeline no matter how long the conversation was.
After: `roots` (1..200) pages by root comment, newest first, with a
(created_at, id) cursor in `next_before` / `next_before_id`. Each page carries
every reply of its roots, so a thread is never split, plus the activities of
the page's time window; `has_more_before` is exact. `around=<commentId>` lands
on the page that starts at the comment's thread root. Requests without
`roots` (including the legacy limit/before/after/around) behave exactly as
before, so installed clients still receive the whole timeline. The issue page
loads the newest 50 roots, fetches an older page when the list scrolls to its
top without moving the viewport, and walks older pages for deep links and
in-page find.
How: three new sqlc queries (roots before a cursor, replies of a set of roots
via a recursive walk, activities in a time window) feed the new
listTimelinePage path. The web cache becomes an infinite query whose newest
page is pages[0]; older pages are its next pages. Realtime and optimistic
patches go through updateTimelinePages in core, which applies a flat-list edit
across pages. Replies per page are capped at the comment hard cap: trailing
threads move to the next page, and only a single oversized thread is cut and
reported through X-Timeline-Truncated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before: opening an issue detail always fetched every sub-issue (even with the
section collapsed) and re-fetched them on every mount, so an issue with many
sub-issues was slow to open.
After: GET /api/issues/{id} returns child_count and child_done_count, the
collapsed header and the unsubscribe control read those, and the list is only
fetched once the section is expanded, 50 at a time, with a "load more" row.
GET /api/issues/{id}/children accepts optional limit/offset (limit 1..200)
and always returns a total beside the issues array; without params it still
returns every child. A backend that predates the counts falls back to loading
the full list as before.
The refetch-on-every-mount guard is replaced by realtime invalidation: child
creation, status change, re-parent and deletion now also invalidate the
parent's detail, and child status patches reach loaded child pages.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
The thread summary test compares last_reply_at against the microsecond
timestamp it inserted, but the handler formatted it with second precision,
so the comparison failed on every run whose fixture time had a fractional
second. Format the field like other ordering fields, preserving what
PostgreSQL stored.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
Before, a feed card needed a full comment list per issue to show reply
counts, open threads and who has been talking.
Now GET /api/issues/thread-summaries?ids=a,b,c (max 100) returns one rollup
per issue in the caller's workspace: counted comments, last comment time,
open (unresolved root) thread count and the three most recent participants.
Deleted and platform-authored (system) comments are not counted. Issues with
no counted comments, or from another workspace, simply have no row.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
TestEveryConcurrentUpBuildHasCleanup requires every CREATE INDEX CONCURRENTLY
up migration to map its index in concurrentIndexCleanups so an interrupted build
is removed before retry instead of leaving an INVALID index behind.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
Expose the open-connector integration over /api/integrations/open-connector
(apps, OAuth and API-key connect, connection list/patch/delete, and the
public OAuth callback), wire it from OPEN_CONNECTOR_* environment
variables behind the open_connector_apps feature flag, add the
owner-only agent.connector_service_allowlist field, and register a
scheduler job that releases stale per-task runtime tokens and expired
OAuth states.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
Add the open-connector business integration: user-owned OAuth and
API-key connections mirrored in user_connector_connection, a DB-backed
OAuth state table so callbacks survive restarts and replicas, and a
per-task runtime token minted at enqueue with allowedConnections pinned
to the agent owner's connections in the agent's service allowlist. The
token id is stored in agent_task_queue.runtime_connector_handles and
released when the task reaches a terminal state; a reaper entry point
releases stale handles and expired OAuth states.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
Standalone SDK for a self-hosted open-connector instance: provider
catalog and setup, OAuth client configs, OAuth and API-key connection
flows, connection deletion, and runtime token management. Responses
are unwrapped per path family (/v1 envelope vs bare /api JSON); secrets
in returned structs are redacted from String, GoString and slog output.
Tests replay recorded responses from a real instance.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
Adds the backend for the operations console: platform_admin roles
(super_admin / operator / viewer), an admin_audit_log table, and
user.disabled_at so disabling an account is a stored state instead of a
hard-coded denylist. The in-process disabled set is now refreshed from the
database every 30 seconds, and the old emergency denylist entries are
migrated into it.
/api/admin exposes overview counts, user and workspace listings with
detail, disable/enable, token revocation, the audit log, and platform
role management. Every write requires a reason and is recorded with the
actor, IP and user agent. PLATFORM_ADMIN_EMAILS bootstraps the first
super_admin.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FrVjAFWeckYV69HS51xUhy
Replace the Composio-specific ComposioOverlayBuilder seam on TaskService
with a ConnectorProviders slice so a second connector backend
(open-connector) can contribute a per-task MCP overlay without touching
the enqueue paths. Composio becomes the first implementation; overlays
from several providers merge by mcpServers key. Behavior is unchanged
for deployments with only Composio configured.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
* fix(execenv): normalize required metadata for Codex user skills
Co-authored-by: multica-agent <[email protected]>
* fix(execenv): preserve user skill frontmatter formatting when filling metadata
Inject missing name/description lines instead of re-marshalling the map, so
comments, key order and CRLF endings survive; present-but-empty values are
replaced on the yaml.Node tree. Fix stale expected values in test messages.
MUL-8095
Co-authored-by: multica-agent <[email protected]>
* fix(execenv): only repair what Codex rejects and verify every repair
Codex falls back to the directory name for a missing/blank name and only
requires a non-empty description, so stop injecting name. The one-line
insert is kept for plain block mappings but now re-parsed; flow, indented
and other mappings go through the node rewrite, which expands aliases of a
replaced anchored value and keeps its comments. Every repair must parse and
leave all other fields equal, otherwise the skill is skipped.
MUL-8095
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: JC的AI分身 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): treat a DSH profile without the runtime as missing
A refused plugin install leaves package.json behind. Counting that file as installed made later probes look transient and blocked self-heal.
Co-authored-by: Cursor <[email protected]>
* fix(daemon): keep a DSH profile directory after a failed install
A failed install must not delete profiles/multica, because that directory may already hold the user's files. A skeleton manifest is already treated as missing, so the next restart retries without probing.
Co-authored-by: Cursor <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
* fix(wecom): re-read the installation's permission at every stage of a file delivery
A revocation is a person withdrawing the connection, and a file delivery is
minutes of work between the top-of-loop gate and the send: the object read,
the chunked upload, the finish. A withdrawal landing anywhere in there
reached a worker with nothing left to consult, so the file went into a chat
whose owner had removed the bot.
The permission is now re-read from the database after the object read,
before EVERY chunk goes to the socket (uploadMedia's new BeforeChunk hook,
asked inside the goroutine that writes so nothing waits between asking and
writing), and again after the upload before the media_id is sent. Three
outcomes rather than a bool: a row that says revoked is final, a read that
failed is this attempt only.
Tests: two unit regressions (a withdrawal during the slot wait stops the
next chunk; a withdrawal before a retry stops the second offer) and a
database-backed one that flips the channel_installation row to revoked
while three chunks are held on the wire, asserting no finish frame and no
send. With the three re-checks stripped, the file is delivered and the
database test fails.
* test(wecom): drop the chunk-retry case whose harness main removed
#8318 deleted mediaConn.dropAcks and the tests that used it, because each
stood still for a whole ackTimeout. This branch still used it, so it built
against its own base and not against main - a break git reports no conflict
for and CI never saw, because these checks ran on 09-02 and #8318 landed on
09-14.
The case is removed rather than restored, and the gap is recorded where the
test was: re-adding the field re-adds the test time that PR was cutting,
and closing it properly needs an ackTimeout seam that did not belong in
this PR uninvited.
* test(wecom): pass the stream store NewOutbound gained in #8355
* fix(wecom): ask the installation's permission at the write, not before the wait
The previous round asked at the start of each stage. Three waits sit between
those questions and the socket, and a revocation landing in any of them still
got its frame out: the socket writer, the chat's turn, and a throttled push's
retry backoff. The failure notice had no check at all.
The permission check is now a write guard carried in the context
(withWriteGuard) and run by wsSender.writeCtx once the writer is held, so the
chunks, upload init and finish, the file message and its retry, and every
notice go through the same question at the last moment. It replaces the
BeforeChunk hook and the three stage checks in sendAttachment. A revoked or
unreadable permission refuses the frame (wrapping errNotAttempted) and the
worker stays silent about it.
request used to wait for the writer on context.Background(), so a caller that
had given up still had its frame written. writeCtx now ends that wait with the
caller's context and writes nothing for a context that is already over; the
ping, handshake and pong keep calling write(), which waits as before. The
socket write deadline is still the connection's own.
Tests: the three orderings from the review (writer, chat lock, throttle
backoff) at sender level and through the whole attachment worker, the notice
at every tellUser call site with a permission-intact control, an unreadable
permission, and the revoke against the real row using testutil fixtures.
* test(wecom): bound the permission read, and count notices by frame so the test holds on main
The permission guard runs while the socket's writer is held, so its read gets
a test that it ends by itself. The notice assertions compared against the
notice constants, which main has since moved into per-language copy; they now
count markdown pushes, which in that rig are exactly the notices.
* fix(daemon): stop sending an English sentence for a cancelled task
A server-cancelled task carried the comment "task cancelled by server".
reportTaskResult forwards that comment as the task's `error`, which the
web UI renders verbatim in the "Technical details" fold and in the chat
"Show details" block — so a French, Japanese, Korean or Chinese reader
got an English sentence under a translated heading.
Nothing is lost by dropping it. The "cancelled" status already yields
failure_reason "cancelled" a few lines later, independently of the
comment, and that code is localized in all five shipped locales
(FAILURE_REASON_I18N_KEYS; "Annulée par le système" in fr). The sentence
said the same thing, only worse, and only in English.
The comment also stops quoting the removed string so a future grep for
it does not lead back to a line that no longer produces it.
No test: this removes a literal on a defensive branch that handleTask's
cancelledByPoll path normally discards, and the surviving behaviour —
failure_reason "cancelled" rendering through failureReasonLabel — is
already covered on the frontend. Verified by build, vet, gofmt and
./internal/daemon, whose two failures reproduce unchanged on main.
* fix(autopilot): use the shared failure reason helper when a task fails or is cancelled
taskFailureReasonForAutopilotRun trims the raw error and falls back to
the localized failure_reason, so an empty task.Error no longer writes
blank failure_reason onto the autopilot run.
* fix(daemon): correct the misleading comment on the cancelled branch
What reaches the server here is the daemon's own context cancellation
(server cancel or daemon shutdown); a user cancel or reassignment is
discarded earlier by handleTask's cancelledByPoll branch.
---------
Co-authored-by: Atelier Nuvio <[email protected]>
Co-authored-by: Hermes Agent <[email protected]>
* MUL-7019 fix(wecom): fetch the picture a sender quoted, not just its label
A quoted picture reached the agent as the four characters [Image]. The
common shape of this in a room is somebody quoting a screenshot of an error
and typing 这个怎么处理 underneath: after #7980 the agent gets the words and
a label where the error was, and answers the label.
The callback carries the quoted attachment exactly the way it carries one
the sender just made - a pre-signed url and its aeskey, under quote.image or
inside quote.mixed.msg_item - so fetching it is the whole of the fix.
Fetching alone would not be enough. The attachment list is a separate block
of text from the body, so with one attachment a reader infers which marker
names which entry and with two it cannot, and a quote is exactly the case
where a message carries somebody else's picture beside its own. A quoted
attachment therefore renders a marker with room in it, [Image: unavailable],
and travels with the marker and its occurrence number so the binder can
rewrite that one occurrence to name the attachment.
It is named rather than embedded. The picture belongs to a message somebody
else sent; replacing the marker with an inline image would state this sender
attached it. channel.MediaRef.InlineIDOnly is what an adapter asks for that
with, and the engine keeps the label ahead of the colon so the marker the
adapter renders and the marker that replaces it stay one shape.
The sender's own [Image] is untouched. It is already unambiguous, being the
attachment on the message the reader is looking at, and changing it would
change what every wecom message body already stored says.
Two assertions from #7980 change with it: a quoted attachment now renders
[Image: unavailable] rather than [Image]. That is the behaviour this change
is for, and a new case pins the sender's own marker beside each of them.
* wecom: number a quoted attachment's marker from the rendered quote, and keep markers through the 500-rune cut
A quoted attachment was bound to its marker by counting the attachments
before it. The engine finds the marker by searching the stored body for the
Nth occurrence of the marker string, so a quoted sentence that spells
"[Image: unavailable]" shifted the real marker's number and the binder
rewrote the sentence instead of the picture. Separately, the 500-rune cut
could remove a marker whose attachment was still fetched and bound, leaving
the quote with no id for the picture next to a newly sent one.
Lay the quote out as runs that carry their attachment, and let one function
decide the cut and stamp each surviving attachment with the occurrence of its
marker in the text as laid out. Words are cut at 500 runes as before; a marker
is never cut or dropped, so the block can exceed the budget by the markers it
carries.
* wecom: name the quoted-marker case that moves only one kind's index
* wecom: build the quoted-marker DB test's rows through the shared testutil fixture
* feat(agent): run Meta Muse Code as an MSP runtime
Muse Code speaks MSP (Muse Session Protocol) over `muse serve`: JSON-RPC
2.0 on stdio, like ACP, but with its own method vocabulary. A session opens
with session/start or session/resume, work is submitted with turn/start, and
assistant text, reasoning and tool calls arrive as transcript items.
The adapter maps those items onto the Backend contract, reads the terminal
turn outcome, and reports a notFound resume as ResumeRejected so the daemon
retries on a fresh session instead of losing continuity silently.
Approval is a session property, not a launch flag: `muse serve` has no
--yolo, and the CLI documents --yolo as TUI-only. A fresh session is created
with approvalMode=allowAll and a resumed session re-asserts it with
session/setApprovalMode, which is what lets a claimed task run without
waiting for a human. The container and per-task worktree remain the
isolation boundary, as they are for every other backend.
commandId must be UUIDv7 — Muse derives a turn id from it and rejects
anything else — so the adapter mints time-ordered ids rather than v4.
The model catalog comes from the session-free model/list query and the pick
is applied through session/start modelId.
* feat(runtime-profiles): allow muse as a protocol family
A custom runtime profile may only be based on a protocol family the daemon
supports, and the database enforces the same list. Widening both keeps the
create flow, the server validation and the CHECK constraint in lockstep with
agent.SupportedTypes.
* fix(muse): complete runtime integration and lifecycle handling (MUL-8057)
Co-authored-by: multica-agent <[email protected]>
* docs(muse): document runtime setup and support (MUL-8057)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* MUL-8070: feat(cursor): support live-run steering via Cursor hooks
Cursor's headless --print mode has no mid-turn input channel, but its
postToolUse/postToolUseFailure hooks can return additional_context that
reaches the model in the same turn. A steerable Cursor run now writes a
managed .cursor/hooks.json whose command is the multica binary in a hidden
relay mode; the relay forwards the hook event to a token-protected loopback
endpoint owned by the run, which hands back pending guidance tagged with a
per-run nonce. The prompt declares that tag so the model treats the context
as genuine user guidance instead of a prompt injection.
Steering is offered only for daemon-managed workdirs on Unix with cursor-agent
2026.10.14+, and the server allowlists cursor for capability negotiation,
supplement creation and composer binding.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* MUL-8070: fix(cursor): confirm steering only on Cursor's receipt and gate on installable hooks
- Count hook-delivered guidance as delivered only when the tool call's
completed stream event echoes it in hookAdditionalContexts. Guidance
Cursor dropped for the same call is retried at the next tool boundary;
guidance without a receipt fails when the turn ends.
- Offer steering only when the workdir can carry the managed hooks.json,
and fail supplements immediately if installation still fails after
negotiation.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>