Files
multica/server/pkg
47f6e970f6 feat(onboarding): write Mika's opening on the server instead of running an agent (MUL-5827) (#6520)
* refactor(onboarding): drop the cross-workspace returning-member branch (MUL-5827)

Mika's onboarding kickoff carried a `returning` flag, set by the frontend
whenever the member was creating an additional workspace. It told the model
"this member has onboarded elsewhere; keep the introduction to one line".

Every workspace onboards from scratch. A member's second workspace may carry
entirely different work, different collaborators, and a different reason for
existing — compressing its opening on the strength of an unrelated workspace
only makes this one worse. The flag was also the single place where one
workspace's state reached into another's first conversation.

Removed end to end: the request field, the prompt note, the API client type,
the bootstrap input, and the `returning: isNewWorkspace` call site. Older
desktop builds that still send `returning` are unaffected — encoding/json
ignores the unknown field, which is exactly the new behavior.

The built-in skill never branched on this, so SKILL.md needs no change.

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

* feat(onboarding): write Mika's opening on the server instead of running an agent (MUL-5827)

The first message a new member ever read cost a runtime cold start plus two
model round trips: the opening was produced by a full chat task, so they
watched a spinner for tens of seconds, and a run that failed introduced Mika
as a red error bubble.

Nothing in that reply needed an agent. The multica-onboarding skill already
pinned it to four beats under a length budget, it reads no workspace state and
calls no tool, and every input it personalizes on — language, workspace name,
agent display name — is already in the request. It also never generated quick
actions (starter cards replace them), so the run bought nothing.

The endpoint now writes two rows in one transaction and enqueues nothing:

  - the opening the member reads, final on arrival, kind onboarding_opening so
    the starter cards still render under it;
  - the hidden kickoff, deliberately WITHOUT a task.

The member's first real message adopts that kickoff into its input batch. That
is what carries the onboarding skill instruction and the profile block into the
run that does the first real work, and — because the kickoff quotes the opening
verbatim — what stops Mika introducing herself a second time. She has no memory
of an opening the server wrote.

The cold start does not disappear; it moves to the member's first real request,
where they are waiting on something they asked for with content and three
one-click starter cards already in front of them.

Three shared invariants that a two-row input batch newly exposes:

  - Both rows are written in one transaction, so DEFAULT now() gives them an
    identical timestamp. The session-list LATERAL picks the last message with
    no tiebreaker and ids are random UUIDs, so a tie could select the kickoff —
    whose kind makes buildChatLastMessage return nil, reporting no last message
    and bringing back the "Start with Mika" card after a perfect onboarding.
    The opening is written one microsecond after the kickoff.
  - DeleteUserChatMessageByTask deleted every user row of a cancelled turn, so
    cancelling the first message would destroy the only copy of the onboarding
    context and could hand the member the product's internal prompt as their
    restored draft. It now excludes the kickoff, and callers release it back to
    unowned so the next send re-adopts it.
  - Both reanchor queries would move the kickoff to dispatch time, putting the
    product's context AFTER the member's message inside one batch. Both now
    exclude it. The regression test fails without that guard.

Also drops the completion-path rule that stamped a kickoff-input turn's reply
as the opening: with the kickoff riding into the first real turn, it would
render the starter cards a second time under a reply that is not an opening.

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

* fix(onboarding): keep stamping the opening for a pre-deploy kickoff turn (MUL-5827)

The server writes the opening now, so no new task produces one — but a kickoff
task enqueued by the previous server can still be claimed by this one during a
rolling deploy, and its reply IS that member's opening. Dropping the stamp
outright left it a plain 'message', and message_kind is persisted with nothing
to recompute it: that member's onboarding session would render without the
starter cards forever.

Gated on "the input batch is a kickoff and nothing else", which is exactly the
old shape. The new kickoff always rides in alongside a real member message, so
the member's first answer still cannot be mistaken for an opening.

Deletable once no pre-deploy kickoff task can still be in flight.

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

* fix(onboarding): release the kickoff when the first turn fails terminally (MUL-5827)

Caught in a live environment, not by a test. The member's first turn died in
preparation, the adopted kickoff stayed bound to that dead task, and their next
message reached Mika with no onboarding context and no record that she had
already greeted them — so she introduced herself a second time, in a
conversation she had already opened. That is the exact failure this design
exists to prevent.

The cancel path already released the kickoff. The failure path did not, and a
first-turn failure is entirely ordinary: runtime offline, agent CLI missing,
timeout, prepare error.

Released in FailTask under the existing retried == nil branch, which is the
terminal-failure condition. Deliberately not on the retry path: a retry child
inherits the root's chat_input_task_id and the kickoff stays bound to that
root, so the retry still reads it — releasing there would strip the context off
a turn that is about to run.

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

* fix(onboarding): hand the kickoff to a queued successor, not to nobody (MUL-5827)

Review catch. Adoption happens inside a send's transaction, so a message the
member queues WHILE the kickoff's turn is still running finds nothing to adopt
and never gets another chance. Releasing the kickoff to NULL when that turn
died therefore left the already-sealed successor to execute with no onboarding
skill, no profile block, and no record that Mika had already greeted them —
the double introduction this design exists to prevent. Worse, a message sent
later could adopt the orphan instead, delivering the context to a turn that had
nothing to do with the opening.

ReleaseOnboardingKickoffFromTask now re-targets the session's next un-started
turn, falling back to NULL only when there is none. Three restrictions on the
target, each load-bearing:

  - status = 'queued' only. A dispatched/running successor has already built
    its prompt from its input batch, so joining it now would consume the
    kickoff without ever delivering it; it waits unowned for the next send.
  - chat_input_task_id = id selects roots that own their input batch. A retry
    child names its root, and the kickoff is already reachable through that
    root, so retries must not be re-targeted.
  - regenerate_quick_actions_for IS NULL skips background suggestion passes,
    which carry no user input.

Ordering matches the shared visible-head selector, so the kickoff lands on the
turn the member will actually see run next.

Covers both directions: A-owns/B-queued/A-fails hands off in the right order,
and an already-dispatched successor is skipped. The first test fails against
the previous NULL-only release.

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

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 18:48:07 +08:00
..