mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 18:06:45 +08:00
admin-workspace-create2
320
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8e68a1b09 | feat(admin): expose market catalog | ||
|
|
1060977bc6 | feat(admin): create workspaces | ||
|
|
38497cc321 | feat(market): support workflow templates | ||
|
|
9472542909 |
feat(web): platform admin console at /admin
Adds the self-built operations UI served from the app origin under the reserved /admin slug: overview, users (with disable/enable and token revocation), workspaces, audit log, and platform admin management. The shell gates on /api/admin/me, sends signed-out visitors to /login, and hides writes a viewer cannot perform; the server enforces the same roles. Core gains an admin API client with zod-validated responses, query options and mutation hooks; views add the pages and copy in all five locales. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01FrVjAFWeckYV69HS51xUhy |
||
|
|
426ac89695 |
feat(market): serve install operations under /api/market
Commit, read, and reconcile share the install ledger. The watcher leases due operations. A missing catalog policy pin writes no row, and workspace delete removes the new tables. Co-authored-by: klinsmaya <[email protected]> |
||
|
|
5145706b07 |
chore(market): drop comments that restate the code
Two comments described rules the types do not enforce. ParseListingID is not the only way to build a ListingID, and decodeVersion returns more than ErrInvalidListing. Co-authored-by: klinsmaya <[email protected]> |
||
|
|
46fbc25c28 |
feat(market): add the workspace client and dark flag
Add Market types, zod schemas, client methods, and query options keyed by workspace. A ready preflight parses only when every check is satisfied and the source is live; anything else falls back to unreadable. Generate the block-reason list from the server JSON with a CI drift check, add the market_v1 flag key, reserve the market slug, and bucket market routes in diagnostics. Co-authored-by: klinsmaya <[email protected]> |
||
|
|
da1671ca20 |
MUL-7420: Project workflows — columns and handoffs on the workflow (#8814)
* feat(workflows): workspace workflows with step handoffs (MUL-7420)
A workflow is a named, workspace-level selection of statuses from the
shared status catalog, in board order, with an optional handoff on each
step. Projects opt in through project.workflow_id; a project without one
keeps the whole catalog (the implicit Default workflow), so existing
workspaces see no change.
- issue_workflow table + project.workflow_id, CRUD API under
/api/issue-workflows, POST /api/projects/{id}/workflow with a dry-run
status mapping plan. Only issues on statuses the target workflow does
not list are remapped, and remapping never hands issues off.
- Issue writes keep the status inside the project's workflow. Entering a
step with a handler through a status change assigns the issue to that
handler and starts its run with the step's brief as the handoff note;
an explicit assignee in the same write wins. Creating an issue starts
at the workflow's starting step.
- Task claims carry the issue's project workflow, and the brief renders
a Project Workflow section so agents move along this project's
statuses instead of the built-in ones.
- Statuses a workflow lists cannot be archived.
- Adoption is gated by the project_workflows_v1 flag; existing
workflows keep working and projects can always switch back to Default.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): workflow settings, project picker and workflow-aware boards (MUL-7420)
- Settings → Workflows (behind project_workflows_v1): the Default
workflow plus named workflows, each shown as its step chain with
handlers and the projects using it; an editor to pick statuses from the
library, order them, mark the starting step, and set each step's
handoff, instructions and next/back statuses.
- Project Workflow property: switching dry-runs first and asks where
issues on unlisted statuses go; nothing else moves and nothing is
handed off.
- A project board's columns are its workflow's steps, and each column
head names who entering it hands the issue to.
- Status pickers offer only the issue's workflow steps and say who each
hands off to; boards refuse drops onto statuses the dragged card's
workflow does not list.
- New issues in a workflow project start on its starting step, with a
hint naming who they will be handed to.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): name built-in steps once in the brief; roomier picker hints (MUL-7420)
Built-in statuses have no stored name, so the handoff brief printed
`todo` todo. Print the key alone when the name adds nothing.
The status picker widens for workflow projects and shows the handoff as
an arrow and name, so step names no longer truncate to one letter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): handoff previews and a single timeline entry per handoff (MUL-7420)
- GET /api/issues/{id}/workflow-handoff?status= says what moving an issue
would do: who it goes to, which runs of its current agent are still
active, and the brief the handler receives. Backs the confirmation.
- POST /api/issue-workflows/preview-brief renders a step's brief from an
unsaved workflow, for the editor's preview.
- Mapping plans list the statuses that stay, so the switch dialog can say
which issues are untouched.
- A handoff is recorded on the status_changed activity (workflow, handler,
whether a run started, the brief) instead of a separate assignee row.
- The brief now reads like the one shown in the design: the workflow, the
current step, its instructions, the statuses, and the commands to hand
off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): rebuild the workflow UI to the reviewed design (MUL-7420)
The first cut used generic controls where the design had purpose-built
ones. This brings every surface up to the mockup:
- Settings → Workflows is one page: the workflows (step chain with
handler avatars, handoff count, projects using it; Default first) and,
below, the shared status library with each status's workflow usage.
- The workflow editor is a full page instead of a dialog: draggable step
cards summarising each handoff, and a panel for the selected step —
swap its status, pick the handler with a segmented control and a
searchable target, handoff instructions, next/back statuses, a preview
of the brief the handler receives, duplicate and delete.
- The project Workflow picker lists each workflow's statuses, handoffs and
projects, links to settings, and always confirms the switch with what
moves and what stays.
- Moving an issue into a handoff step (status picker, table, board drop)
asks first: who it goes from and to, a default-on option to stop the
previous agent's run, and the brief.
- Status pickers name the workflow and project, show each step's handler
with an avatar or that the assignee stays, and explain themselves.
- Boards: the handler chip in project column heads, hatched columns a
dragged card cannot enter with the reason, and a drop hint naming who
the card will go to.
- The timeline records a handoff under its status change, with the brief
the run received; the issue sidebar shows the workflow it follows.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): word a queued previous run as queued in the handoff confirm
The confirm dialog said the previous agent was "still running (for N
minutes)" even when its task had only been queued. Queued runs now read
as queued, and the checkbox cancels them instead of stopping them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): refuse a run's status change once the issue left its step
A run decides where the issue goes next from the step it was queued for.
When someone else moved the issue while the run worked, applying that
decision undid their move: in the live Grok run, a PM parked NW-1 as
blocked and the still-running agent moved it straight back to code
review.
Tasks now record the issue's status when they are queued
(agent_task_queue.workflow_step, written by the task INSERTs; automatic
retries keep their parent's step), and a run's own status changes move
it along. In a workflow project, an agent status change from a run whose
step no longer matches the issue is refused with 409
workflow_step_moved, naming where the issue is now and who moved it, so
the agent stops instead of retrying.
A handoff away from a squad now also stops the runs made on the squad's
behalf, and stopped runs record who stopped them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): keep every status entry point inside the issue's workflow
The issue context menu listed every catalog status and applied handoff
steps without the confirmation the status picker and board show, so a
right-click could pick a status the workflow refuses or start an agent
unasked. It now offers the workflow's steps and opens the handoff
confirmation through a shared modal.
One-click status actions no longer assume built-in statuses a workflow
may not list: "mark done" lands on the workflow's done step (hidden when
it has none), un-marking a duplicate returns to the workflow's start,
and "mark duplicate" is offered only when the workflow lists cancelled.
The batch toolbar offers a single project's workflow statuses when the
selection sits in one project.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): keep issue wakeups consistent with workflow steps
Three places where project workflows and issue wakeups met:
- A wakeup run is queued when it fires, so it took the issue's status at
that moment as its workflow step. A wakeup an agent registered then
bypassed the stale-step refusal: parked by a PM, the issue could be
moved on by the agent's "check CI, then hand to review" wakeup. It now
keeps the registering run's step; a member's wakeup still starts from
the issue's status.
- Retries carried handoff_note only for wakeup runs, so a retried
workflow handoff lost its brief. Rows with a workflow step keep it.
- Switching or editing a workflow remaps statuses in bulk without the
close rule, so an issue remapped into a done status kept its wakeups
and pending wakeup runs. The remap now ends them like any status write.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(wakeups): don't also wake the agent a write hands the issue to
A workflow handoff or an assignment already starts the receiving agent's
run. If that agent also subscribed an event wakeup to the issue's status
or assignee, the same write queued a second run for one fact, and
workflows made such writes frequent.
The write now names the agent it hands the issue to in the
transaction-local multica.handed_to_agent (the new agent assignee, a
squad's leader, or a step's handler even when it already owned the
issue), and wakeup capture skips that agent's subscriptions for the
write's issue.* events. Other agents' wakeups, and task, comment and
reaction events, are captured as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): tell agents not to watch workflow issues with wakeups
The handoff brief and the runtime's project workflow section now say
that each step's handler gets a run when the issue enters the step, so
wakeups on a workflow issue are for waiting on time or on something
outside it, not on its status, assignee or runs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): refresh project usage after project changes
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): MUL-7420 split columns from per-project handoffs
Workflows now only define a board's columns between a fixed head
(backlog, todo) and tail (blocked, done, cancelled). Handoffs move to
project.workflow_handoffs, so projects can share columns and still hand
off to different people. Default becomes a real, editable row that every
project without a named workflow follows. Adds project handoff, add-column
(project fork or shared) and project brief preview endpoints; same-agent
adjacent columns re-run the agent.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(core): MUL-7420 project handoffs and Default workflow client model
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 column-header handoffs and project workflow menu
Board column headers set the project's handoff for that column; the
project toolbar gets a workflow menu (full flow, switch, templates) and an
add-column action. Views outside a project show Default's columns, with
other statuses reachable through a status filter. Adds the issue flow card,
create-project workflow choice, a columns-only workflow editor and a
lighter status picker.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): MUL-7420 wrap create-project workflow label in a menu group
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): MUL-7420 keep workflow structure in settings; no squad handoffs
Projects no longer add columns or create workflows: the add-column
endpoint is removed and creating a workflow is admin-only. Handoffs go
to one agent, a member or the project lead; squads route work themselves,
so a handoff to one is rejected, as is the issue-creator handler.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* refactor(core): MUL-7420 drop the project add-column client
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 simpler handoff picker and workflow editor form
The handoff form is one searchable "Hand to" picker (no handoff, the
project lead, an agent or a member); the next status defaults to the next
column and next/back overrides sit under "More options". The workflow
editor gets labelled name and description fields and shows the fixed
statuses as one start and one end row. Templates move from the project
menu to Settings → Workflows → New, and the project board loses its
add-column action.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* refactor(server): MUL-7420 keep handoffs on the workflow, make backlog and blocked optional
Handoffs move back onto workflow steps: a workflow is the whole pipeline,
and a project only picks one. The project lead stays a relative handler, so
projects can still share a workflow. Handoff edits are admin-only, through
the workflow write or PUT /api/issue-workflows/{id}/handoffs/{status}; the
brief preview moves to the workflow too. Drops the unreleased
project.workflow_handoffs migration.
Todo, done and cancelled are required; backlog and blocked are optional and
keep their fixed places. Without blocked, the brief points agents at the
step's back status.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Cursor <[email protected]>
* feat(views): MUL-7420 edit handoffs on the workflow, optional backlog and blocked
The workflow editor sets each column's handoff in its side panel, and
Backlog and Blocked switch on and off. The board's column popover and the
flow panel edit the workflow's handoff, say how many projects it reaches,
and are read-only for non-admins. Drops the per-project handoff client
code.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(wakeups): MUL-7420 keep platform rules on writes that hand off nobody
Migration 579 skips an issue.* event for the agent a write hands the issue
to. Its filter compared agent_id with the transaction's handed_to_agent
setting using IS DISTINCT FROM, which is false when both are NULL. Main's
platform rules (child_done) have no agent_id, so every issue.* event that
handed the issue to nobody was dropped for them: a parent leaving backlog
never woke its agent for sub-issues that closed meanwhile
(TestChildDoneHeldWhileParentInBacklog).
Rules without an agent are never the handoff target, so they always
capture. Extends TestHandoffDoesNotAlsoWakeTheHandler with a platform rule
on a handoff and on a plain status change; the latter fails on the old
filter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 three-way handoff choice; quieter move confirm
The handoff form picks one of three directions, as in the mockup: no
handoff, hand to an agent (starts a run), or hand to a member, project lead
included (assignee only). What the step does, where it goes next and the
back status only reach an agent's brief, so they show and save for agent
handoffs alone.
The board's move confirmation keeps the agent's brief collapsed. Create
project shows the workflow picker even when Default is the only workflow,
with a note on where to make more. The hidden-columns panel says a board's
columns come from its workflow, with a link to manage workflows for admins.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(workflows): confirm handoff on project move, per-project handoff copy, two-pane editor (MUL-7420)
- A single issue moved into a workflow project from issue detail confirms
the handoff of the column it lands on; the write opts in with
enter_workflow_step. Batch, API and CLI moves stay administrative.
- The handoff preview takes project_id to preview such a move.
- Saving a shared workflow's handoff from a board can keep the change to
that project: the server copies the workflow for it and switches the
project in one transaction (copy_for_project_id).
- The column popover says which workflow it edits and who else uses it.
- The workflow editor is a step list with a detail panel for the
selected step.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* style: gofmt workflow tests
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 show next/back always; edit workflow name and description in place
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(views): MUL-7420 trim copy on the workflows settings page
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix: MUL-7420 keep pre-workflow behavior for workspaces that never adopt workflows
The run step guard now applies only to workflows that hand off, and the
standalone Statuses settings page keeps its original header.
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(views): MUL-7420 use a solid placeholder tone on the workflow header
Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(workflows): keep the runtime brief cache-stable; fix project board and batch status gaps (MUL-7420)
- Move the project workflow out of the runtime brief into the per-turn
prompt so CLAUDE.md stays byte-identical across steps and issues.
- An explicit status filter overrides the project workflow's columns; the
hidden-columns panel lists statuses outside the workflow.
- Batch status offers only statuses every selected workflow allows; a
partial batch rolls back, refetches and warns.
- setProjectWorkflow returns void instead of an unparsed body.
Co-authored-by: Cursor <[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]>
Co-authored-by: Lambda <[email protected]>
Co-authored-by: Cursor <[email protected]>
|
||
|
|
130aaa5302 |
MUL-8047: feat(onboarding): open the runtime guide issue directly instead of a welcome modal (#9289)
* MUL-8047: feat(onboarding): open the runtime guide issue directly instead of a welcome modal Co-authored-by: Cursor <[email protected]> Co-authored-by: multica-agent <[email protected]> * MUL-8047: chore(onboarding): drop dead option cards, copy, and completion animations Co-authored-by: Cursor <[email protected]> Co-authored-by: multica-agent <[email protected]> * MUL-8047: fix(onboarding): reopen the existing guide issue on duplicate A reload, or a create whose response was lost, leaves the guide issue from an earlier Skip in place while onboarding is still incomplete; the retry then hits the server's active-duplicate guard and the step was stuck toasting forever. Recover the existing issue from the validated 409 body and continue to completion. 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]> |
||
|
|
86020b88a4 |
MUL-8047: chore(server): remove the pre-v3 desktop onboarding shim (#9291)
Co-authored-by: Cursor <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
231dedb552 |
MUL-8034: chore: clean historical comments across the codebase (#9274)
* chore: clean historical comments (MUL-8034) Co-authored-by: multica-agent <[email protected]> * docs: preserve pending Composio expiry contract Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
e326a561ae |
MUL-8000: fix(agents): persist builder draft warning dismissal (#9265)
* fix(agents): persist builder draft warning dismissal Co-authored-by: multica-agent <[email protected]> * fix(agents): preserve builder warnings during automatic model cleanup Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c6b451c9c1 |
fix(views): route plain attachment download links through the re-sign path (MUL-7991) (#9211)
* fix(views): route plain attachment download links through the re-sign path (WK-11) A plain markdown link to the API-host attachment endpoint (MULTICA_PUBLIC_URL/api/attachments/<id>/download, persisted by buildMarkdownURL in private-storage deployments) opened with a bare navigation. The browser sent no credential there — web's session cookie is host-locked to the API origin and SameSite=Strict, and Desktop/mobile carry a bearer token no <a> can send — so the request returned 401 "missing authorization" and the user downloaded the error body, not the file. File-card/image and the download button already route through useDownloadAttachment, which refreshes authenticated metadata and navigates to the credential-bearing URL the server mints per click (capability / presign / CloudFront). Plain links missed that path. Intercept attachment download URLs (UUID-guarded via attachmentIdFromDownloadURL) in RichLink and hand the click to the same download hook, so all three attachment entry points behave alike. Modified clicks still open natively; the raw URL remains the durable ref. Add a routing test covering the UUID-link interception and the non-UUID non-interception. Fixes WK-11. * fix(attachments): scope Markdown downloads to their deployment and workspace Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Dev (multica) <agent@multica> Co-authored-by: Yushen <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3d2592a9a2 |
MUL-7754: feat: local search index for web and desktop (#8891)
* feat(server): add a change log and sync endpoints for local search (MUL-7754) Web and Desktop will keep a per-workspace copy of issue, comment, and project text for instant search. Triggers on issue/comment/project record the xid of each entity's latest write in search_index_change, covering every write path including hard and cascaded deletes. Clients bootstrap with GET /api/search-index/manifest + /snapshot pages and catch up with POST /api/search-index/changes, whose cursor is a pg_snapshot: a change is returned once it is visible in the target snapshot but not in the one the client holds, so late commits are never skipped and long transactions do not stall catch-up. Workspace teardown skips the triggers and clears the table, and an hourly scheduler job prunes rows older than 30 days; a cursor older than the prune mark gets 410 and rebuilds. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(core): add a synced local search index for web and desktop (MUL-7754) Each (user, workspace) gets an IndexedDB copy of its issue, comment, and project text, kept in a worker (SharedWorker when available, otherwise a dedicated worker per tab) and searched in memory. Matching, ranking, and snippets port the server's issue/project search, so local rows have the server's shape and order. The worker bootstraps from /api/search-index/manifest + snapshot pages, catches up through /changes after realtime events, reconnects, focus, and every five minutes, and resumes an interrupted bootstrap. It has no credentials of its own: tabs run its requests through their API client. Local results are served only while the copy is complete and recently confirmed; otherwise searchIssues/searchProjects fall back to the server. Workspaces over 500 MB of text, lost access, and expired cursors are handled by declining, wiping, and rebuilding respectively. Session cleanup deletes every local index. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(views): search through the local index first (MUL-7754) The command palette, @mention suggestions, the issue picker, and duplicate suggestions now call the core local-first search. The palette drops its 300 ms debounce while the local index is serving, since no request leaves the tab. An E2E spec checks that palette results match server ranking without any server search request, follow issues created and deleted through the API, and that logging out deletes the local copy. .env.example documents the FF_LOCAL_SEARCH_INDEX kill switch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(core): close local search gaps found in review (MUL-7754) - Access loss: deleting, leaving, or being removed from a workspace now destroys its local copies right away (worker and tab both delete the database, since a full-page relocate may end the worker first). The sync hook also prunes every copy outside the user's current workspace list, which covers access lost while offline or on another device; copies a tab is showing are kept in case the list is stale. - Memory budget: the engine counts UTF-8 bytes the way the manifest does, and the budget is checked on every load, snapshot page, and catch-up batch, not only at bootstrap. An over-budget copy is dropped and search falls back to the server until the workspace is measured again. - Frozen tabs: a port the sweep dropped is restored with its workspace the next time the tab sends anything, a page restored from the bfcache attaches again, and a null local answer clears the tab's "serving" flag so the palette goes back to debounced server search. E2E now also covers being removed from a shared workspace. 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]>
|
||
|
|
104bf7be35 |
MUL-7732: regroup settings by scope and consolidate scattered pages (#8874)
* feat(mcp): report how many agents use each workspace MCP server The workspace MCP library listing now carries agent_count: the number of live (non-archived) agents each entry is assigned to. Settings shows it so an admin can tell whether replacing or removing a server affects anyone. The field is optional in the client schema, so an older server that omits it simply shows no count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(settings): regroup settings by scope and consolidate scattered pages Implements the MUL-7732 settings redesign: - Navigation is grouped by who a setting affects: Personal, the current workspace (with Issues & workflow and Connections & extensions sub-groups), and This device on desktop. Pages and sections carry a scope badge (account, this device, workspace), and members see a lock on pages only owners and admins can change plus a shared read-only notice. - Settings search in the nav indexes page and row titles and descriptions; opening a result scrolls to and highlights the row. - Preferences is one page instead of three tabs, with a searchable time zone picker, a segmented theme control and a field grid for the create-issue dialogs. - Code merges Repositories, GitHub and self-hosted Git. Repositories are read-only rows edited through dialogs; self-hosted Git connects in a dialog that ends on the one-time webhook secret. - The PR merge rule moves to Statuses & transitions as an automatic transition; Code only shows its current value. Built-in statuses show edit and archive as unavailable instead of refusing after a click. - Integrations becomes Messaging (IM channels only) with a breadcrumb on detail pages; Composio moves to Personal as Connected apps. - Members gets tabs, search, invite and share-link dialogs and a seat summary. MCP servers show assignment counts and explain how MCP, connected apps and plugins differ. Workspace General shows members plain values and changes the issue prefix through an explicit dialog. - Skill labels are managed from the Skills page; Settings > Labels keeps the issue catalog. - Successful saves no longer toast; failures still do. Old ?tab= values (github, repositories, integrations, issue, chat, labs, and the Composio OAuth callback) resolve to the new pages, since server redirects still use them. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * docs: point settings paths at the regrouped settings pages GitHub, repositories and self-hosted Git now live under Settings > Code, IM bots under Settings > Messaging, and the PR merge rule under Settings > Statuses & transitions. The GitHub guide also drops the old master switch step in favor of pausing from the GitHub row. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): edit the PR merge rule on the Code page The "After PRs merge, move the issue to" rule sits with the other pull request rules on Code again instead of a separate Automatic transitions section. It applies to every code host, so pausing GitHub features does not lock it. The statuses page goes back to Issue Statuses, and its merge badge links to the rule on Code. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): manage skill labels under Settings → Labels again The Skills page entry was buried at the bottom of the filter menu's Labels submenu. Settings → Labels switches between the issue and skill catalogs again, using the members page's segmented switch (now SettingsViewTabs), and the Skills page keeps its shortcut. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): keep GitHub resume reachable after a disconnect Pausing GitHub features is a workspace setting, but the pause and resume item lived in a menu that only rendered with an installation. A workspace paused before disconnecting, or with github_enabled=false and no installation, lost the way back, and every PR switch stayed disabled. On deployments without a GitHub App it could not even reconnect, though Co-authored-by works without one. The menu now shows whenever GitHub is connected or paused, and only Disconnect requires an installation. The paused note shows in either state. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): let the URL choose the open members list The members page read `section` only to initialize its list tab, so a search result for invitations or share links opened while already on the page changed the URL but not the list, and back/forward did nothing. With a router the open list is now derived from the URL, and switching lists replaces `section`. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): tell repositories on different ports apart The manual repository dialog reused the GitHub import's identity, which drops the port, so https://git.example.com:9443/acme/api.git read as a duplicate of the same path on :8443 and could not be saved. Non-default ports now stay in the identity; default ports, including 22 for ssh://, still compare equal to the scp-like form. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * test(comments): stop the recipient-menu tests flaking under CI load The steering tests opened a recipient chip menu while the composer could still re-render its chips, from the previous send settling or the preview catching up. That closed the menu, and the menu steps also used the 1s default wait inside the 5s default test timeout. Under parallel load locally, about 1 in 20 runs failed the same way as CI. Menu choices now go through a helper that reopens the menu until the item shows. The remaining waits get the 5s the first steps already had, and each test gets 15s. 140 of 140 loaded runs pass. 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]> |
||
|
|
815fe37b1f |
MUL-7685: fix(agents): paginate task history and aggregate duration (#8804)
* fix(agents): paginate task history and aggregate duration * test(agents): cover pagination boundaries and query lifecycle * fix(migrations): register agent history index retry cleanup * fix(agents): show totals only for complete task history * fix(agents): average duration over the full rolling window The server bounds duration totals by completed_at > now() - 30 days, which can span 31 calendar days. deriveAgentActivity summed duration only after trimming buckets to the chart's 30 local-day slots, so runs on the oldest partial day were dropped from the average. Sum duration for every returned bucket before slotting. Co-authored-by: multica-agent <[email protected]> * fix(migrations): renumber agent history index to 552 main now carries 551_pr_merge_status, and migration numbers from 129 onward must be unique (TestMigrationNumericPrefixesAreUnique). Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
fc4a615700 |
MUL-7726: let the workspace choose the status merged PRs move an issue to (#8862)
* feat(pr): move issues to a workspace-chosen status when their PRs merge (MUL-7726) The merge automation completed an issue only when a PR said "Closes MUL-1", so the workspace switch mostly did nothing: most PRs are linked by title or branch and never completed anything. The workspace now picks what a merge does, in settings.pr_merge_status: "none", or the key of a started or done status (custom ones included, Blocked excluded). Absent means Done. When every linked PR is merged, the issue moves to that status. A closing keyword only links. - Decision: terminal, triage, "none", the per-issue switch and a new at_target state (the issue already has the target) come before the PR states, so the issue page only speaks when a merge would move the issue. The response adds target_status. - MoveIssueFromPullRequests replaces CompleteIssueFromPullRequests and writes the target status. The target is resolved through the delivery's shared catalog read (Resolver.WritableCategory). An archived or unknown choice, or a failed read, fails closed to no write. - Close intent is no longer synced or read. The column stays. - Migration 551 pins "none" on existing workspaces whose history shows a merge never moved every issue: auto-complete switched off, or a linked PR whose merges were not all keyword merges. Workspaces with no links, or with only keyword merges, keep the Done default. Replays are harmless. - CLI: `multica issue pr-automation <id> on|off` lets an agent keep one issue where it is. The multica-platform skill describes the new rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(views): choose the status merged PRs move an issue to on the GitHub page (MUL-7726) - Settings → Integrations → GitHub → Features: "After PRs merge, move the issue to" picks Don't change, or a started or done status, custom ones included and Blocked excluded. It replaces the read-only auto-complete row. The self-hosted Git page shows the same setting. The statuses page drops its switch, and a badge on the target status now links to the setting. - Issue sidebar: the line under the PR list names the target status ("Moves to In Review when #19 merges"). It says nothing when the workspace leaves status alone, when the issue already has the target, or on an older backend's no_close_intent. The per-issue switch reads "Keep status when PRs merge" and is hidden when it would change nothing. The menu opens the GitHub setting. The link hint only shows while auto-link is on. - core: derivePRMergeStatus replaces derivePRAutoCompleteEnabled; the auto_complete schema adds target_status and the at_target state. - Copy in 5 languages. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * docs: describe the workspace-chosen status for merged PRs (MUL-7726) GitHub integration, self-hosted Git, issues and environment variable pages (4-5 languages) now say that the workspace picks what a merge does: Done by default, another started or done status, or no change. They also say that a closing keyword only links, and how to keep one issue's status. The troubleshooting entries explain what a missing line under the PR list means. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * refactor(pr): keep the merge automation out of agents' instructions (MUL-7726) Agents don't act on what a merge does to an issue, so they don't need a command for it or a rule to learn. Drop `multica issue pr-automation`, and cut the multica-platform skill down to how PRs get linked: no merge rule, no auto_complete field guide, no "the server moves it to done" note. People still keep one issue's status from its Pull requests menu, and the docs now point there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(pr): keep the retired auto-complete switch working for old desktop clients (MUL-7726) Review of #8862: an admin on a desktop client from before this change can still flip "Complete issues when their PRs merge". The client saved pr_auto_complete_enabled = false, and the save looked successful, but the server only read pr_merge_status, so merges kept moving issues to Done. - Settings writes: a flip of the retired switch becomes a choice (off → none, on → Done). An old client that echoes a chosen target without flipping the switch leaves the target alone. Every write mirrors the switch from the effective choice (false for none, absent otherwise), so old clients show the right state. - Reads: without pr_merge_status, the retired switch decides (off → none, else Done), on the server and in derivePRMergeStatus. This also covers settings written by a pod that predates the change during rollout. - Migration 551 sets the switch off on the workspaces it pins to none. 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]> |
||
|
|
e2f4a22030 |
feat: steer a running agent from the normal composer, per recipient (MUL-7631) (#8760)
* feat(server): steer running turns from ordinary comments (MUL-7631) A comment can now name recipients whose running turn should receive it (`steer_agent_ids` on POST /issues/:id/comments). Each named recipient with a running, steer-capable turn gets the comment bound to that turn instead of a follow-up run; any other recipient keeps its normal queued / coalesced / deferred trigger, so losing a race never drops input. - One comment can steer several turns: receipts are keyed by (comment_id, task_id) and returned as `supplements` on comments and timeline entries. The single supplement_* fields mirror the first receipt for older clients. - Completion reconciliation skips a steered comment only for the agent it steered, so the same comment still reaches recipients that were addressed without steering. - Delivery claims match the (comment, run) pair. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(issues): choose per recipient how a message reaches a running agent (MUL-7631) Steering moves from a separate "Add message" box on the run row into the normal reply and comment composers. Recipients keep the existing trigger rules; each recipient's current run on the issue decides what the message can do: - running (and steer-capable): add to the current run (default in that run's own thread), start after this run, stop and start over, or skip - queued: include when it starts, or skip - idle: start on send, or skip Receipts follow the message ("Waiting for Lambda to read it" → "Read by Lambda · after step N"), a failed delivery offers retry or sending it as a new run, and the run's step list marks where it read the message. A draft whose recipient finished meanwhile stays in place and says it will start a new run; a message with files is handled after the run. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(server): steer only the chosen turn, and make a retried steer idempotent (MUL-7631) - Steering names the exact running turns the author chose (`steer_task_ids`). A chosen turn that ended before the send arrives is never swapped for a later turn of the same agent; that recipient keeps its normal trigger. - A steering send carries `client_request_id`. A retry after a lost response returns the original comment (200) instead of creating a second comment and a second delivery; a concurrent twin binding the same request is treated as already steered. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): guard steering sends against stale previews and broad resends (MUL-7631) - "Stop and start over" never stops a run while the trigger preview is still catching up with an edited @mention: the stop is dropped unless the preview answers the submitted text. - Steering sends the chosen turns' task ids and one request id per draft text, reused on retries of the same text. - "Send as a new run" on a failed receipt reaches only that receipt's agent: every other current recipient is suppressed. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): keep the reply box placeholder neutral (MUL-7631) The empty reply box cannot know who a reply goes to: the author may @ someone else or only talk to a teammate. The recipient chip already says what Send will do once there is text. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * test(issues): give steering composer tests room for the preview debounce (MUL-7631) Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(issues): choose the default reply to a running agent (MUL-7631) Preferences > Comments gains "When replying to a running agent": "Add to current run" (default) or "Start after this run". It only changes what a reply does without a per-message choice; the recipient chip still offers both, and replies that never steered by default (another thread, an idle or queued agent) are unaffected. Saved on this device next to the sticky comment bar. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(server): save one comment per send and honor "don't start" on replay (MUL-7631) Idempotency now lives on the comment, not the delivery receipt. A send's client_request_id is stored on the comment behind a partial unique index (migrations 550/551), so a retry, or a concurrent twin, returns the saved comment (200) whether the send steered a running turn or fell back to a normal trigger. Before, a twin could save a second comment, and a send whose turn had ended kept no receipt to find. The agents an author chose not to start are stored on the comment too, and completion replay skips them. "Send as a new run" for one missed recipient, and "Won't start this time", no longer wake the other agent once its current run completes. An edit re-records the choice. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): repeat the original steering request on retry (MUL-7631) Until a send is settled, retrying the same text sends its first steering request unchanged, even after the chosen turn ended. The server then returns the comment the first attempt saved instead of posting it again as new input. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(migrations): fail fast on locks in 549 and 550 (MUL-7631) Both take an ACCESS EXCLUSIVE lock on a table the product touches continuously: comment for every read and write, task_supplement for every daemon claim. Bound lock acquisition and execution, as 483 does for issue, so a long transaction makes the migration fail and retry on the next run instead of queueing all traffic behind it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(server): steer at most one turn per comment during rollout (MUL-7631) Steering is live, and servers from before per-run receipts claim every receipt of a comment at once. While they still serve daemons during a rollout, a comment bound to two turns could have the second copy marked in flight and never delivered. Until every server claims per (comment, run), a comment steers the first chosen turn only; later recipients keep their normal trigger. A follow-up lifts the limit after this ships. Also covers a steer_task_ids entry that names another issue's turn. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(server): finish a saved send on retry; keep an edit's choices with its text (MUL-7631) A send's comment was saved before its agents were started or steered, and deduplication treated the saved comment as the whole send. A dropped connection cancelled the request between the two, and the retry only returned the comment, so no agent ever received it. - Attempts of one logical send now run one at a time under a transaction advisory lock, held on its own connection: a twin waits for the attempt in progress (up to 30s, then 409) instead of racing it, and a process that dies releases it. - After the comment is saved, dispatch runs on a non-cancelable context, and comment.client_request_dispatched_at (migration 552) records that it finished. A retry that finds a saved but undispatched comment finishes the dispatch with the comment's recorded choices. An edit's "don't start" choices were written by a separate statement after the text, so interleaved edits could leave an older edit's choice. They are now part of UpdateComment, under the same revision check as the text. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): offer one steered recipient per message during rollout (MUL-7631) The server steers at most one turn per comment until #8800, but the composer showed "Add to current run" for every running recipient and the server quietly queued the rest. Now only one recipient steers: the one the author picked, otherwise the first that would by default. The others show "Start after this run", and picking "Add to current run" on one of them moves the steer there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(server): claim a send before dispatching it; drop the request lock (MUL-7631) The previous recovery marked a send dispatched only after reaching its agents, so a failure after dispatch made a retry reach them all again. It also held a pooled connection per send for an advisory lock while the send needed more from the same pool, which could wait on itself until timeout. A send's comment now holds a dispatch claim instead (comment.client_request_dispatched_at, one conditional update): only the attempt whose claim takes effect publishes the comment and reaches its agents. A failed claim writes nothing else and returns 500 for the client to retry; a retry that finds an unclaimed comment claims and dispatches it; a claimed one is only returned. Concurrent attempts no longer wait on each other, and post-save dispatch keeps a 30s bound of its own. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(server): let one comment steer several running turns (MUL-7631) Lifts the rollout limit from #8760. Every server now claims receipts per (comment, run), so a comment can be bound to each running turn its author chose, and each recipient receives it in its own run. Merge only after #8760 is deployed everywhere. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(issues): steer every running recipient a message addresses (MUL-7631) The client side of lifting the rollout limit: each running recipient can take the message in its current turn again, instead of only one per message. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * refactor(issues): drop send-retry idempotency and two display extras (MUL-7631) A steering send now behaves like any other comment when its response is lost: a retry posts it again. The machinery that tried to make that retry return the saved comment grew with every edge it met, and ordinary comments never had it: - Remove comment.client_request_id, its unique index, and the dispatch claim (migrations 551/552, and 550 reduced to suppressed_agent_ids). - Remove the replay/resume path in CreateComment, the post-save non-cancelable dispatch context, and the client's sticky request id. Steering sends only `steer_task_ids`; each binding uses the comment id as its receipt request id. Two display extras go too, since the UI already shows the same thing: - The "run ended while you were writing" notice: the recipient chip already switches from "Add to current run" to its new action. - "after step N" on a read receipt: the run trace already places the message at the step where it was read. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): space agent names in Chinese copy; neutral recipients header (MUL-7631) - The steer badge joined names with Intl.ListFormat, which gives "Lambda和Orion" in Chinese. Latin names are now spaced from the Chinese joiner ("Lambda 和 Orion"), per the zh copy conventions; the enumeration comma stays unspaced. - The multi-recipient popover was still titled "Will start when sent" and its tooltip said "choose who runs", though each row now picks how that agent handles the message. Title it "Recipients" and reword the tooltip. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(issues): translate the recipient source labels in Chinese (MUL-7631) "assignee" and "@mention" were left in English in zh-Hans; the recipient menu header shows them. Use 负责人 (per the zh conventions) and @提及. 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]> |
||
|
|
9df0cae78a |
MUL-7429: complete issues when every linked PR merges (#8758)
* feat(pr): complete issues when every linked PR merges (MUL-7429) Title/branch identifiers link a PR; keywords and the body no longer matter. Completion is evaluated only on a PR event for the issue (merge, link, unlink), so settings changes and reopening never complete an issue. Adds manual link/unlink with remembered exclusions and a per-issue auto-complete switch. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(pr): issue-page PR automation and single auto-complete setting (MUL-7429) The issue's Pull requests section says what the merge rule will do next, links a PR by URL, removes one per row, and turns auto-complete off for that issue. The workspace switch lives under Issue statuses; the GitHub page reports it. Agent skill and docs describe the title/branch rule. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(pr): accept PR links pasted without a scheme (MUL-7429) Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * test(skills): pin the PR auto-complete contract in the platform skill (MUL-7429) Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(pr): re-check linked PRs at the completion write; skip stale GitHub events (MUL-7429) The status write now requires every linked PR to still be merged when it runs, so a PR linked between the decision and the write keeps the issue open. GitHub PR upserts ignore events older than the stored row, like the self-hosted path, so a late delivery cannot roll a merged PR back to open and turn a redelivered merge into a new 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]> |
||
|
|
1539c54ec5 |
feat(issues): make duplicate marks traceable and one click from the original (MUL-7349) (#8741)
* feat(issues): make duplicate marks traceable and one click from the original (MUL-7349)
- Log duplicate_marked / duplicate_unmarked on the duplicate and
duplicate_added / duplicate_removed on its original, with the other issue
linked in the activity feed; the delete and GitHub merge paths now carry
both ends of the mark so those rows are written too.
- Expose duplicate_of_issue_id on issue responses (only while cancelled) so
list rows, board cards and table cells can show an arrow to the original
that opens it directly.
- Banner links as a whole to the original and shows its status; the undo
action becomes the secondary "Not a duplicate" with a toast.
- Sidebar duplicates show reporters and a count; the status picker reads
"Change original" on an issue that already is a duplicate; the mark
picker opens with same-title and recently viewed candidates.
* fix(issues): keep the duplicate pointer in step with CI shape tests (MUL-7349)
- Add duplicate_of_issue_id to the CLI --fields whitelist, which the
list-endpoint shape test checks against a real response.
- Project the column in the search parity test's legacy query copy and
scan it, so both queries read through the one scanner.
- Mount the row marker's query and navigation hooks only when the issue
is a duplicate, so surfaces rendered without a QueryClient (the table's
inline title) and thousands of ordinary rows subscribe to nothing.
* fix(issues): resolve a duplicate's original on the server and log one row per mark change (MUL-7349)
- Issue responses carry duplicate_of {id, identifier, title, status}, filled
beside the status category only while the issue is cancelled and the
original still exists; the bare pointer is no longer exposed. Rows read it
directly, so the marker makes no request and is a real link where the row
is not one.
- A mark change suppresses the generic status_changed row; the unmarked row
records where the status went. Covered through the real UpdateIssue path,
with the test server wiring activity listeners like main.go.
- The picker applies one selectability predicate to search results and
suggestions, so an existing duplicate can no longer be chosen.
|
||
|
|
93ae552ba9 |
feat: add messages to active Claude Code and Codex runs (MUL-7565) (#8682)
* feat: add live run supplements (MUL-7565) Co-authored-by: multica-agent <[email protected]> * fix: address MUL-7565 review feedback Co-authored-by: multica-agent <[email protected]> * fix: resolve MUL-7565 supplement panel and receipt review Co-authored-by: multica-agent <[email protected]> * refactor: remove MUL-7565 supplement ordinals Co-authored-by: multica-agent <[email protected]> * fix: close supplement composer after run remount * fix: exclude task supplements from completion reconciliation * fix: build a persistent binary for local daemon launches * feat: support Claude task supplements through SDK hooks * refactor: simplify task supplement delivery * test: remove redundant task supplement coverage * fix: remove redundant supplement delivery notices * refactor: extract local daemon launcher fix to its own PR * fix: preserve ordinary comment size compatibility * fix: accept empty task start acknowledgements * fix: bound task start response decoding and retries --------- Co-authored-by: Forge-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> Co-authored-by: yushen <[email protected]> |
||
|
|
96db419c0a |
feat: mark an issue as a duplicate from the status picker (MUL-7349) (#8728)
Enable step. The previous release added duplicate_of_issue_id and the
code that keeps it correct; this one lets people create a mark.
PUT /api/issues/{id} accepts a write-only duplicate_of_issue_id, which
sets status to cancelled and so reuses every status side effect
(status_changed event, stage barrier, wakeups). Marks are validated
under row locks on both issues: the target may not itself be a duplicate
and an issue with duplicates may not be marked, so the relation stays
one level deep and concurrent cross-marks cannot form a cycle.
GET /api/issues/{id}/duplicates returns both sides, and applies the
validity rule itself so a rollback past the previous release still reads
correctly.
The status picker on an existing issue gains a 'Mark as duplicate...'
action in its footer, outside the arrow-key listbox so it is never
mistaken for a status. A duplicate shows a banner linking to its
original with an unmark action; the original lists its duplicates.
Copy added for en, zh-Hans, ja, ko and fr.
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
90e0bdf830 |
MUL-7586 Atomically create issues with custom properties (#8689)
* feat(issues): create with properties atomically Co-authored-by: multica-agent <[email protected]> * fix(issues): recover unavailable properties in create drafts Co-authored-by: multica-agent <[email protected]> * test(issues): isolate commit failure task assertions Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Forge-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8c9f865e3e |
MUL-7480: Revert comment steering feature (#8648)
* Revert "MUL-7480: Link delivered steers to final replies (#8623)" This reverts commit |
||
|
|
3de6275eb3 |
MUL-7480: Link delivered steers to final replies (#8623)
* feat: link steer receipts to final replies Co-authored-by: multica-agent <[email protected]> * fix: keep steer final reply locators stable Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Forge-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f8c90a31d4 |
MUL-7546 fix(issues): preserve identifier lookups across remounts (#8622)
* fix(issues): preserve identifier lookups across remounts * docs(core): clarify issue lookup cancellation Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Forge-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c375f83c2e |
MUL-7418: add issue event and time wakeups (#8467)
* feat: add issue event and time wakeups (MUL-7418) Co-authored-by: multica-agent <[email protected]> * feat(wakeups): capture issue collaboration event lifecycle Co-authored-by: multica-agent <[email protected]> * fix(wakeups): preserve reply source when reopening threads Co-authored-by: multica-agent <[email protected]> * feat(wakeups): summarize sidebar and issue activity cues Co-authored-by: multica-agent <[email protected]> * fix(wakeups): restore disabled configurations with a two-way toggle Co-authored-by: multica-agent <[email protected]> * feat(wakeups): manage workspace wakeups in Autopilot Co-authored-by: multica-agent <[email protected]> * chore(wakeups): reserve migrations after current main Co-authored-by: multica-agent <[email protected]> * test(wakeups): align inventory fixtures with shared contracts Co-authored-by: multica-agent <[email protected]> * fix(wakeups): distinguish trigger conditions and execution results Co-authored-by: multica-agent <[email protected]> * fix(wakeups): suppress registration feedback and drain bounded evidence Co-authored-by: multica-agent <[email protected]> * fix(wakeups): align sidebar actions with titles Co-authored-by: multica-agent <[email protected]> * fix(wakeups): bound database work and unify shared issue visibility Co-authored-by: multica-agent <[email protected]> * feat(wakeups): filter mutation events by member or agent actor Co-authored-by: multica-agent <[email protected]> * refactor(wakeups): merge structured evidence and narrow receipt locks Co-authored-by: multica-agent <[email protected]> * feat(wakeups): let people edit wakeup instructions Co-authored-by: multica-agent <[email protected]> * fix(wakeups): align inventory controls and compact prompt editing Co-authored-by: multica-agent <[email protected]> * fix(mobile): handle deferred agent run status Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
8ce795b007 |
MUL-7480: Steer active agent turns from comments (#8605)
* feat: steer active agent turns from comments (MUL-7480) Co-authored-by: multica-agent <[email protected]> * fix: make comment steering hint driven Co-authored-by: multica-agent <[email protected]> * fix: harden active comment steering races Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Forge-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
2aa20b0353 |
ZIC-245: fix custom Oh-My-Pi runtime profile identity and discovery (#8576)
* fix(runtimes): preserve custom profile base runtime identity Co-authored-by: multica-agent <[email protected]> * fix(runtimes): pass both url params to the profile identity test TestUpdateRuntimeProfile_RejectsIdentityChange nested two withURLParam calls. That helper installs a fresh chi route context each time and does not copy what is already there, so the outer call for profileId dropped id: the handler answered 400 "workspace_id is required" and never reached the immutability check the test exists to cover. Use withURLParams, which carries existing params forward and is what the rest of this file already uses. Co-authored-by: multica-agent <[email protected]> * fix(runtimes): name a runtime target the same way on every surface The base-runtime picker spelled Oh-My-Pi with an inline ternary, so it was the only place that used the product name. The catalog row, both detail headers, the base-runtime detail row and the locked chip in the form kept rendering the stored id under `capitalize` — a user picked "Oh-My-Pi" and the next screen, and the saved profile, said "Omp". Move the name into runtimeTypeLabel next to the target whitelist and read it from every surface. profileRuntimeType replaces the repeated runtime_type ?? protocol_family fallback. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: Bohan-J <[email protected]> |
||
|
|
67cec3fe3f | fix(transcript): pair parallel tool results by call identity (#8567) | ||
|
|
d7009df8cc |
Revert "MUL-7300 fix(issues): preserve Quick Create original input (#8519)" (#8546)
This reverts commit
|
||
|
|
a6472044c3 |
MUL-7300 fix(issues): preserve Quick Create original input (#8519)
* fix(issues): preserve quick-create original input Co-authored-by: multica-agent <[email protected]> * fix(issues): validate quick-create origins Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
1fcdff78d0 |
MUL-7436 feat(auth): slide UI session expiry instead of forcing a re-login every 30 days (#8486)
* feat(auth): slide UI session expiry instead of fixing it at login A session token's exp was stamped once at login and never moved, so every continuously active user was logged out 30 days later, always mid-task. The session now re-issues itself once it drops below half its TTL, turning that scheduled logout into an idle bound. Renewal is stateless: it copies the claims of a token we signed and moves exp, so there is no session table and no DB read on the hot path. - Browsers are renewed inline by middleware.Auth on safe requests, keeping the session HttpOnly — no client change and no multi-tab coordination. - Clients holding the session as a string (Desktop, mobile) get POST /api/auth/refresh, which only ever accepts a UI JWT. PATs, agent task tokens and cloud-node PATs are refused: a machine credential must not become an interactive session. An expired JWT cannot be revived. - CSRF tokens are now bound to a new sid claim, stable across renewals, so re-issuing the auth cookie no longer invalidates a CSRF token another tab already read. The pre-existing binding stays valid so sessions that predate this keep working until they renew. - CloudFront cookies are re-signed alongside a renewal, so the CDN policy cannot outlive its session. The idle bound is one-sided and the comments say so: a session last used while it still had over half its TTL left is not extended, so the guaranteed survivable gap is TTL/2, not TTL. Refs MUL-7436 Co-authored-by: multica-agent <[email protected]> * feat(auth): renew the session on Desktop, mobile, and legacy token-mode web Completes the client half of sliding sessions. Browsers on cookie auth need nothing — the server re-issues their cookie inline — so this covers the clients that carry the session as a string and have to persist a new one. Renewal follows use, not a clock. Checks run at launch, on returning to the foreground, and on interaction; there is deliberately no interval timer, because a timer would keep an abandoned but still-open window (or a backgrounded phone) signed in forever. The server decides whether a session is renewable and tells the client when to ask again, derived from the deployment's configured TTL — so no client reads exp, hardcodes a cadence, or has to reason about clock skew. Renewal is not a session event, and each step protects that: - Concurrent triggers collapse into one request, so two valid tokens never race to be the one stored. On mobile the single-flight flag is set before the first await, since reading the Keychain is itself async. - A response that lands after logout or an account switch is discarded by comparing against the session the attempt started from; writing it back would resurrect a session the app has already torn down. - Offline, 5xx and an unreadable body all keep the current session, which is still valid for at least another half TTL. Only a real 401 ends a session, and that path is untouched — nothing here can clear tabs or drafts. - WebSocket clients now read the token at connect time instead of pinning the one captured when they were built, so a reconnect after a renewal authenticates with the current credential. Web also retries a rejected CSRF token once. Session-bound CSRF makes that unreachable in the steady state; it covers the single transition where a session predating that binding is renewed for the first time. Refs MUL-7436 Co-authored-by: multica-agent <[email protected]> * fix(auth): address review blockers on sliding session renewal Six fixes from review, each with a regression test that fails without it. An expired session was answered 403, not 401. The CSRF gate runs before the JWT is validated, and it read `sid` through the expiry-enforcing parser — so an expired cookie lost its sid, its perfectly good CSRF token failed to verify, and the user's first action after expiry was a rejected write instead of a return to the login page. Reading the session id and deciding access are now different questions with different parsers: the first only needs the signature, the second still enforces expiry. Rolling back was not survivable. A previous release can verify only the token-bound CSRF binding, so once a session was issued or renewed by this one, a rollback left every signed-in user able to read and unable to write, with no client retry able to help. Both bindings are now issued: clients echo both, the server accepts either, and the token-bound cookie is re-minted on every renewal so a rollback taken at any moment still finds a usable pair. Mobile could resurrect a session it had just ended. Keychain deletes are async, so a renewal that sampled storage after logout began but before the delete landed would write its replacement back. A session epoch, incremented synchronously at the start of logout, a 401 teardown and a new sign-in, closes the window comparing tokens cannot — including the case where logout lands during the renewal's own write, which is now undone rather than left behind. Mobile only checked at launch and on foreground, so an app held in the foreground and used continuously would never check again and could expire under the user's hands. Every touch now passes through a capture-phase responder that declines the gesture it observes. CloudFront cookies were re-signed by a middleware not every renewing route mounts — the plugin bridge renews without it, and would have slid the session forward while leaving the CDN policy behind. Re-signing moved next to the renewal itself, so the two cannot diverge by route. The renewal check interval could exceed the window it has to land in: a flat five-minute floor meant any TTL under ten minutes produced a cadence longer than the renewal window, logging out clients that followed it exactly. The cadence is now capped at half the threshold, which outranks both clamps. Refs MUL-7436 Co-authored-by: multica-agent <[email protected]> * fix(auth): close the CORS, credential-write and short-TTL gaps Three blockers from the second review round. The two CSRF values now travel in one header instead of two. A second header name would have to be in the server's CORS allowlist, and that list is version-specific: a browser holding a cookie this release issued, talking to a rolled-back server, would send a header that server never allowlisted. The preflight fails, the request never reaches a handler, and no retry helps — precisely the half-logged-in state the second cookie exists to prevent. With one header name the server just tries both bindings, and the client switches to the token-bound value when a server rejects the session-bound one. It switches back as soon as the cookie changes, keyed on the rejected value rather than a flag, so it cannot oscillate once per renewal. A test now pins the CSRF header to the allowlist so a future second header fails here rather than in production. Mobile credential writes are serialized, and a stale renewal is refused rather than undone. Compensating after the fact meant deleting whatever was stored — which, after a logout and a fresh sign-in, is the new account's token: the app would be logged in in memory and signed out on the next launch. The check and the write now happen inside one serialized unit, so a logout or a sign-in cannot land between them and a refusal writes nothing at all. AUTH_TOKEN_TTL has a documented one-minute minimum. Clients are told when to check again in whole seconds, so a shorter TTL asks the wire to carry a sub-second cadence: it truncates, and below about four seconds it serialises to 0. Values under the minimum are clamped up with a startup warning rather than honoured. Mobile's floors were never actually lowered in the previous commit — 1h/1m, not the 5s/5m claimed — so both clients now floor at 5s, below anything the server can send, and fall back to 30s, which fits the smallest supported window. The cadence tests assert the serialised integer a client actually receives, not the internal time.Duration. Refs MUL-7436 Co-authored-by: multica-agent <[email protected]> * fix(auth): keep a failed renewal cheap and a renewed token shared Two blockers from the full-PR review, plus the documentation the config change owes. A failed check no longer consumes the normal cadence. The two were one number, so a single 503 spent whatever the server had last suggested — with the 30-day default that is three days. A client told to check again in three days on day 12, returning on day 29 with one day of session left and hitting one 503, would not have been allowed to retry until day 32: two days after the session it was trying to save had expired. Failures now buy a short backing-off delay of their own, reset by the next success, so recovery follows the network rather than the cadence. The bearer token is read from shared storage per request instead of cached per client. Desktop runs one ApiClient per window over one localStorage, so a session renewed in one window left the others sending a credential on its way out until it expired and took the whole session down. A 401 is also now matched against the credential that produced it: a request that went out just before a renewal can be answered after it, and treating that as expiry would tear down a session that is demonstrably alive, clearing tabs and drafts with it. Documentation catches up with both the behaviour and the new minimum, in all four languages: sessions are an idle bound rather than a countdown from sign-in, the bound is one-sided, and AUTH_TOKEN_TTL clamps up at 60s. The rationale for that minimum is corrected too — it is the 5s floor every client applies to the derived cadence, not whole-second serialisation, which only binds far below it. Also removes two claims that were not true: that a failed renewal leaves at least half a TTL (it happens inside the window, so less), and that an open untouched window never renews (cookie-mode sessions slide on any authenticated request, background polling included). Refs MUL-7436 Co-authored-by: multica-agent <[email protected]> * fix(auth): back off on a renewal response that carries no cadence A 200 whose body fails schema validation does not throw — parseWithFallback returns zeroes — so both clients reset the failure count without moving the next-check deadline, and every subsequent interaction fired another request at a server already answering badly. A missing cadence is now treated as a failure and takes the same short backoff; a token that did arrive is still applied. Also settles the documentation wording: the 30 days is not an idle timer. Renewal only happens past the halfway point, so an unused session can end sooner than 30 days after its last use, and saying otherwise promised precision the design does not have. Refs MUL-7436 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
f0c9146c4b |
MUL-7437: fix(api): classify disabled capabilities as non-retryable (#8487)
* fix(plugin): return 403 when API is disabled (MUL-7437) Co-authored-by: multica-agent <[email protected]> * fix(api): classify disabled capabilities as non-retryable (MUL-7437) Co-authored-by: multica-agent <[email protected]> * fix(api): address disabled capability review feedback (MUL-7437) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3fe16b8fcb |
MUL-7422: paginate archived inbox and load it on demand (#8471)
* fix(inbox): paginate archived notifications on demand Co-authored-by: multica-agent <[email protected]> * fix(inbox): isolate archived deep-link lookup states Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
904693bed9 |
MUL-7336: feat(labels): add skill label CLI commands and skills list label filtering (#8368)
Brings Skill labels to the CLI and makes them usable for finding skills in the web app. - `multica label` becomes resource-type aware (`--resource-type issue|skill`, `--description`), and `multica skill label list/add/remove` manages labels on a specific skill. Short IDs now resolve against the right catalog. - The skills page toolbar gains a Labels filter; `GET /api/skills` bulk-attaches labels so the list does not go N+1. - Agent labels are deliberately excluded, keeping the decision from #6245. - CLI docs updated in all four locales. |
||
|
|
f639d35f44 |
fix(agents): MUL-7383 exclude cancelled runs from success metrics (#8424)
* fix(agents): exclude cancelled runs from success metrics * refactor(agents): require activity outcome counts from the back-end The previous commit carried a fallback for a new client talking to an older back-end: optional `completed_count` / `cancelled_count`, null propagation through the bucket series, and a "—" success rate when the counts were absent. Multica only supports the other direction — an older daemon or client against a newer back-end — so that branch is an unrequested legacy adapter. Outcome counts are now required at the schema boundary, so the bucket series and window summary carry plain numbers. A response without them is contract drift and `parseWithFallback` degrades the whole list to `[]`, the same as every other list endpoint. Unchanged: a window holding nothing but cancellations still has no success rate and renders "—" rather than claiming 100%, and the sparkline still draws cancellations neutral. Both are the point of the fix, not compatibility. Copy for the unavailable state now says the period has no completed or failed run, which is the only way it is reachable. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9d18186e6e |
fix(status): MUL-7365 expand category compatibility before backfill (#8407)
* fix(status): stage category expansion and preserve installed client groups (MUL-7365) Co-authored-by: multica-agent <[email protected]> * fix(status): validate mixed storage with GC and recovery compatibility Co-authored-by: multica-agent <[email protected]> * docs(status): align phase one rollout with merged main dependency Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
7dafc0cd62 |
MUL-7240: Unify issue lifecycle into four stored categories (#8261)
* feat: simplify issue status categories Co-authored-by: multica-agent <[email protected]> * feat: adopt four stored issue lifecycle categories Co-authored-by: multica-agent <[email protected]> * fix: preserve visible work when upgrading saved category preferences Co-authored-by: multica-agent <[email protected]> * docs: align lifecycle comments with stored and wire contracts Co-authored-by: multica-agent <[email protected]> * test: align mobile grouping expectations with four categories Co-authored-by: multica-agent <[email protected]> * fix(issues): group user views by concrete status keys Co-authored-by: multica-agent <[email protected]> * fix(settings): simplify status editor copy and category selector Co-authored-by: multica-agent <[email protected]> * feat(statuses): allow ordering built-ins and unify row actions Co-authored-by: multica-agent <[email protected]> * feat(statuses): configure custom status icons independently of category Co-authored-by: multica-agent <[email protected]> * fix(statuses): dismiss built-in status notice Co-authored-by: multica-agent <[email protected]> * fix(issue-status): require moving issues before archive and polish controls Co-authored-by: multica-agent <[email protected]> * fix(ci): renumber issue status migrations after main updates Co-authored-by: multica-agent <[email protected]> * fix(statuses): isolate archive inspection and align catalog behavior Co-authored-by: multica-agent <[email protected]> * fix(statuses): respect injectable catalog for table grouping Co-authored-by: multica-agent <[email protected]> * refactor(statuses): simplify built-in skill and runtime guidance Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a9e82c7973 |
MUL-7282: fix(comments): delete only the selected comment and keep its replies (#8323)
Deleting a comment removed every reply under it, through the parent_id ON DELETE CASCADE. Now delete removes only that comment: one with replies becomes a tombstone (deleted_at set, body/attachments/reactions/resolution cleared) so the replies keep their parent, and one without replies is removed along with any tombstone ancestor left empty.
Tombstones are read-only and are never agent input. Every write to a comment's children takes its owners first (issue -> comment -> child), and a comment commits together with the attachments it was posted with. Clients only promise that replies are kept when the server declares comment_delete_keep_replies_supported, and then delete through DELETE /api/comments/{id}/keep-replies, which older servers do not route.
|
||
|
|
b36906e6d5 |
MUL-6967 perf(inbox): derive the unread badge from the summary endpoint, not the full list (#7924)
* perf(inbox): derive the unread badge from the summary endpoint, not the full list
The sidebar nav badge, the desktop dock badge and the mobile tab badge were
all computed by downloading the entire inbox with `GET /api/inbox` and
counting it client-side. That query has no LIMIT and no pagination, so every
app start pulled every unarchived notification the user has — each carrying a
full untruncated comment body — to render one number, whether or not the user
ever opened the Inbox.
`GET /api/inbox/unread-summary` already returns that number: one small row per
workspace, and its SQL applies the same newest-per-issue rule
`deduplicateInboxItems` applies before render, so the count is unchanged. Web
already fetches it for the workspace-switcher dot, so the badge there now
costs no request at all; mobile trades an unbounded list fetch for this one.
`GET /api/inbox/unread-count` is deliberately not used: it counts raw rows, so
one issue with three unread notifications would read as 3 where the inbox
shows a single row. Documented on the client method so the trap is visible.
The badge is now server-computed, so the optimistic list patches no longer
move it on their own. Each mutation that patches the list re-derives the
workspace's summary entry from the patched rows through the same dedup helper
the inbox renders through — the badge still moves in the same frame and
cannot disagree with the rows on screen. Mutations that cannot predict the
outcome invalidate the summary on settle instead, which they now all do: it
lives under an account-level key that `inboxKeys.all(wsId)` does not reach.
Refs MUL-6967.
Co-authored-by: multica-agent <[email protected]>
* fix(inbox): make the unread badge server-owned and resolve tabs by object
Addresses review on #7924.
The badge no longer derives from the list cache. Recomputing a workspace-wide
count from `deduplicateInboxItems(listCache)` and writing it into the summary
read as instant, but a list cache proves only that the list was loaded once —
never that it is complete or concurrent with the summary — and the
account-level summary query is not covered by the workspace-scoped
`cancelQueries`, so a response already in flight lands on top of the local
value anyway. Under pagination it would be wrong by construction: one loaded
page cannot produce a global count. Rows stay optimistic; the badge follows
the server, which costs a round-trip and buys a single writer.
`issue:deleted` now refreshes the summary. It is an `issue:*` event, so no
`inbox:*` handler runs to pick it up, and the summary query is
`staleTime: Infinity` with no refetch on focus — deleting the last unread
issue emptied the list and left the badge lit with nothing to correct it. The
invalidation lives inside the updater on both platforms so no call site can
forget it.
Inbox tab titles resolve per object instead of through the list. `selectedKey`
IS the key the inbox groups by (`issue_id ?? id`), so for an issue-backed
notification it already IS the issue id — the hop through the list row was
redundant, and it was the only reason a restored tab needed the whole Inbox
fetched first. An issue-less notification still reads its row, since its title
exists nowhere else, and an unresolved selection now keeps the tab's persisted
title as its first frame rather than collapsing to "Inbox".
A selection whose row lives in the other view now titles by object rather than
resolving to "Inbox": treating "not in the page I loaded" as "does not exist"
is what this should not do, and the page already redirects such a deep link to
the issue.
Every added test was confirmed failing on
|
||
|
|
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
|
||
|
|
3dcfdaea41 |
feat(issues): simplify board defaults and sorting (#8256)
* feat(issues): simplify board defaults and sorting Co-authored-by: multica-agent <[email protected]> * fix(issues): address board review feedback Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c03633d64a |
MUL-7202: fix(usage): distinguish unreported agent runs (#8241)
* fix(usage): distinguish unreported agent runs Co-authored-by: multica-agent <[email protected]> * fix(usage): handle dashboard rollup lag Co-authored-by: multica-agent <[email protected]> * fix(usage): preserve nonterminal usage totals Co-authored-by: multica-agent <[email protected]> * fix(usage): expose truncated coverage details Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
e04465be0b |
feat(transcript): tell the reader when a tool output was truncated MUL-7168 (#8212)
* MUL-7166 MUL-7167 feat(transcript): tell the reader when a tool output is incomplete A tool_result preview is cut at 8 KiB and the remainder is never uploaded. Nothing recorded that, so the run transcript rendered a truncated preview exactly like a complete one — a reader checking what an agent did could take the first 8 KiB of a 300 KB log for the whole thing. The daemon now reports whether the preview dropped bytes, and the viewer says so. Two separate claims, kept separate: - Per step: a "truncated" badge next to the tool name when the record provably lost bytes. - Per run, once: a caveat when the run contains output whose completeness nobody measured. The flag is tri-state end to end (NULL/absent = unknown, false = complete, true = truncated) and `output_truncated` is nullable with no default and no backfill. Historical rows cannot be reconstructed, so they stay unknown; the same applies to messages from an older installed daemon, which is why the batch insert carries the value through the query's text[]/NULLIF encoding rather than a []bool that cannot express the third state. Display clipping is now worded as what it is. The inspector's 8000-character clip said "... (truncated)", the same word the badge uses for bytes that are gone, when copy still yields the whole stored text. Verified: go test ./internal/handler ./internal/daemon ./internal/util ./internal/migrations, pnpm test (4981 views + core), typecheck, lint. Both states captured in a local environment against real API + DB rows. Co-authored-by: multica-agent <[email protected]> * refactor(transcript): move the completeness note to the end of the output The truncation indicator was a warning badge beside the tool name and a run-level banner above the step list. Both announced the caveat before the reader had a reason to care, and the badge's colour gave a routine 8 KiB cut the weight of a failure. "Was that all of it?" is a question you only have once you reach the bottom of an output, so the answer goes there: one muted line at the end of the body, in the same place the display-clip line already sits. The unknown case follows the same rule rather than keeping its own banner — the inspector shows one step at a time, so a note there appears once per opened step, not once per turn in a list. That was the whole objection to the per-turn disclaimer this design replaced; a run-level bar was one way to avoid it, and putting the note where the evidence is another. `outputCompleteness` replaces `hasUnknownOutputCompleteness`: the same rules (tool results only, empty output excluded, false is a measurement) now answer per item instead of per run. Co-authored-by: multica-agent <[email protected]> * fix(transcript): stop the display-clip line promising the whole output "copy to get the whole record" reads as "the full output is in the database". It is not: the daemon cuts at 8 KiB before reporting, and copy returns the stored record — the same bytes, minus the 8000-character render clip. The first reader of this string took it the other way, which is the whole reason the truncation note exists. Says what copy actually returns: everything that was saved. Co-authored-by: multica-agent <[email protected]> * fix(transcript): leave a stored tool result with one remark, not two The render clip cut at 8000 characters, but a stored tool result is already capped at 8192 bytes upstream — so on an ASCII output the clip could only ever shave a further ~190 characters, and it did so under a note that already said the rest was not saved. Two grey lines, the second reporting a rounding error on the first. The clip now sits above the source budget, where a tool result cannot reach it. Tool input keeps it: input is persisted whole and has no server-side budget, so it is the body that actually needs bounding — and it never carries a completeness note, so that path stays a single remark too. The marker also stops mentioning copy. It described the clip's reversibility, which is only interesting next to a truncation note that is no longer beside it. Co-authored-by: multica-agent <[email protected]> * fix(transcript): say why the output is short, not where it stops "Output ends here" reads two ways: the command produced this much, or we cut it. Those are opposite facts about the agent's run, and the note exists to settle exactly that question. States the cause instead: the output was too long, so only the beginning was kept. "Kept" rather than "shown" — the rest was never uploaded, and wording it as a display limit invites the assumption that the whole output is in the database and merely hidden. The unknown note loses the same phrase for the same reason. Co-authored-by: multica-agent <[email protected]> * fix(transcript): address review on the completeness note Three fixes from review of #8212, plus the frequency question it reopened. A malformed `output_truncated` deleted the whole run from the screen. The field parsed with `z.boolean().optional()`, so a string or a number failed its row, the row failed the array, and `parseWithFallback` handed the viewer an empty transcript. It now catches at the field, degrading to unknown. The old test claimed this but passed a malformed top-level object, which exercised a different path; the replacements fail without the catch. The note appeared before the reader could have wanted it. `ToolDetailSurface` collapses its own body, so a footer rendered outside it sat over a faded, half-shown output — answering "was that all of it?" to someone still reading. The surface now owns the note and withholds it until the body is revealed. The render clip went back to 8000 characters for tool input. Raising it to 20000 was collateral: the goal was to stop re-clipping a tool result, which is already capped at 8192 bytes upstream, and skipping the clip for `tool_result` alone does that without touching how an unbounded input renders. Unknown-ness moves back to the run. One daemon produces every message in a run, so either it measured that run's outputs or it measured none of them — a fact about the run, stated once, rather than a remark repeated on each step opened. Co-authored-by: multica-agent <[email protected]> * fix(transcript): drop the notice for records that predate the flag A record from before the flag existed now says nothing. The viewer speaks only when a daemon measured an output and found bytes missing. The column stays tri-state. Nothing renders differently for NULL and false today, but they are not the same fact, and defaulting NULL to false would write "this daemon measured the output and stored all of it" onto rows where nobody measured anything. The original lengths are gone, so that claim could never be corrected — a lie the schema would keep telling long after this UI changed again. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
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 |
||
|
|
728d26f0b5 |
MUL-6211: feat(agents): expose usage in task history (#7085)
* feat(agents): expose usage in task history * fix(agents): surface task usage query failures * perf(agent): bound task usage history queries * fix(agents): paginate task usage history safely * fix(migrations): repair interrupted keyset index builds * refactor(agents): make task usage opt-in * docs(agents): clarify task usage availability Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Multica Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
70ea4c8f0d |
MUL-7050 perf(search): remove unused exact search counts (#8033)
* perf(search): remove unused exact issue count Co-authored-by: multica-agent <[email protected]> * perf(search): remove unused project count Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
7d339c186f |
fix(auth): send the user to login when the session expires (MUL-7028) (#7993)
* fix(auth): send the user to login when the session expires A 401 mid-session only dropped the stored token: the auth store kept `user` set and `status: "authenticated"`, so neither shell's login gate (desktop `App.tsx`, web `[workspaceSlug]/layout.tsx`) ever fired. The shell stayed mounted and, with the token now gone, every following request went out unauthenticated — the user got a wall of "missing authorization" toasts and no way forward. Add `sessionExpired()` to the auth store: an idempotent teardown that lands in exactly the state a cold boot with a dead token reaches. The api client's 401 hook drives it, and AuthInitializer's boot rejection now delegates to it, so both dead-session paths converge on one implementation. Mobile has done this since its own 401 handler. The login page says why the user is there, rather than reading as the app having dropped their work for no reason. The claim is only made when this client actually presented a credential the server rejected — a first visit's identity probe 401s too. Two follow-ons the new path exposed: - Drop the query cache on an involuntary session end. Every query has `staleTime: Infinity`, so whoever signed in next would render the dead session's data and never refetch it away. Explicit logout already did this; the involuntary path did not, and it could not be reached before. - Snapshot the CLI-authorize token before probing the cookie. The probe is expected to 401 for a token-mode session, and the teardown clears the very key the fallback then looks for, so token-mode users could never authorize a CLI. Co-authored-by: multica-agent <[email protected]> * fix(desktop): keep the daemon running when the UI session expires Desktop's `onLogout` stops the local daemon, and routing session expiry through it meant an expiring UI credential killed a process that was authenticating with its own PAT and might be running agent work. Split the two events. `onSessionExpired` defaults to `onLogout`, so a shell only needs its own handler when part of its logout teardown is too destructive for an expiry it did not ask for — which on desktop is the daemon, and the tab layout with it. An account switch stays safe because it is handled at the other end: `daemon:sync-token` mints a fresh PAT and restarts the daemon whenever the user id changes. The no-token boot exit runs the same teardown rather than the logout one. Otherwise the daemon survived the expiry and then died on the next app start, when a missing token was read as the user having signed out. Both desktop teardowns move to platform/session-teardown with their side effects injected, so the difference between them is testable without an Electron window — verified failing when expiry stops the daemon. `AuthInitializer` no longer takes `onLogout`: every unauthenticated exit now delegates to the store, which owns both callbacks. `initCore` takes a named options object, since onLogin / onLogout / onSessionExpired are three adjacent `() => void` that only argument order told apart. Co-authored-by: multica-agent <[email protected]> * fix(auth): erase the previous session's client state on expiry Review found two holes in the new expiry path. The expiry only dropped the query cache, while logout also resets in-memory drafts, clears per-workspace persisted state, the tab layout and the last-workspace cookie. But the screen an expiry lands on offers a login form, and the next person to use it is not guaranteed to be the one who was just signed in — and when they share a workspace, the post-login stale-tab validator finds nothing to prune. A colleague on a shared machine would inherit drafts, chat selection and tab titles: the exact leak drafts/cleanup-registry exists to close. So an expiry now erases exactly what a logout erases. The teardown moves to core's platform/session-cleanup, shared by both, and desktop's expiry path resets tabs too — leaving the daemon as the only difference between the two, which is the point of the split. Unsent drafts are lost on an expiry now, the same as on logout; keeping them would mean gating the restore on user identity, which the login page cannot decide before it has one. Second, the idempotence guard sat above the credential teardown, so a login attempt that 401s — Desktop's deep link writes a token, then verifies it — never left "unauthenticated" and returned with the invalid token still in storage, to be replayed at the next launch. That was a regression against the old 401 handler, which always removed it. Dropping the credential now happens first and unconditionally; the guard only suppresses the parts that end a session. Both regressions verified failing before the fix. Co-authored-by: multica-agent <[email protected]> * fix(auth): clean up the previous session on a cold-start rejection The cleanup only ran on a transition out of `authenticated`, and it enumerated the keys to remove from the workspace list in the Query cache. The case that leaks worst satisfies neither: the app starts, the stale token on disk is rejected at the identity probe, and no workspace list was ever loaded — so A's drafts and chat state sat on disk waiting for whoever signed in next. Key it on being unauthenticated instead of on leaving `authenticated`, and enumerate the stored keys rather than the workspace list. Storage adapters gain an optional `keys()`; workspace-scoped entries are `${base}:${slug}` and every base is known, so the slugs no longer have to be. That also catches workspaces the last session had left. This cannot sign anyone out. Reaching `unauthenticated` IS the decision to end a session, and only a 401 on a presented credential or having no credential at all gets there — a client that cannot reach the server settles on `recovering` and keeps its token and its drafts. Pinned with regression tests for offline, 500 and 504. Also closes the test gap that hid this: the "no workspace list" case asserted only the global tabs key, so it passed while every per-workspace key survived. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |