Commit Graph

8 Commits

Author SHA1 Message Date
Bohan Jiang
ba108978ac fix(channel): deliver the final answer only to Slack and Lark (MUL-5378) (#6016)
* fix(channel): deliver the final answer only to Slack and Lark (MUL-5378)

Channel replies could carry the agent's interim narration alongside its
answer (GH #6006). Two independent causes, both fixed at the layer that
owns the deliverable.

Prompt regression. #4776 told every channel-backed chat to reply with the
answer only and not narrate. The MUL-4899 split (#5557) moved that rule
into the Slack branch along with the `chat history` / `chat thread`
commands its wording happened to mention, so Feishu/Lark silently lost it
on 2026-07-17. The rule is a third axis — it keys off "is there a channel
at all", like the attachment-upload axis — so it now sits outside the
Slack gate, generalized from "these history reads" to any progress note.
The old two-layer matrix could not catch the regression because it only
asserted the rule on the Slack case; the new test pins all three states.

Runtime contract. Result.Output is documented as "final user-facing output
selected by the backend" (agent.go), but Codex concatenated every
agent_message and Copilot joined every assistant turn with "\n\n", so a
tool-using run handed the daemon narration + answer as one string. Codex
now takes the message its app-server labels phase="final_answer", falling
back to the most recent agent message on the legacy protocol; Copilot
keeps the latest complete turn, with the streaming deltas retained as the
process-died-mid-turn fallback. This narrows delivery only — every message
still streams as MessageText, so the Multica transcript is unchanged.

Claude Code, CodeBuddy and qwen already selected a terminal result and are
untouched; opencode/deveco/openclaw share the accumulating shape and want
the same audit (pi was already fixed this way in #4894).

Verified: go build ./..., go vet, full ./pkg/agent and ./internal/daemon/...
suites pass locally.

Co-authored-by: multica-agent <github@multica.ai>

* fix(channel): scope the no-narration rule to process, not results

Review follow-ups on the channel delivery rule and the Copilot turn
boundary.

The prompt said a reply "must not say what you are about to do or just
did", which literally forbids the deliverable itself: asked to create an
issue, the correct reply IS "created issue X". Rewritten to ban planned
and in-progress narration while explicitly protecting the completion
confirmation. The test now pins both halves — a future edit that drops
the carve-out, or restores the blanket past-tense ban, fails.

The example also referenced "check the code", which is not a thing an
agent does inside a Slack or Lark conversation. Replaced with a generic
"let me look into that first".

Copilot cleared pendingDelta only when the authoritative assistant.message
carried content. A tool-only turn reports content:"" with the requests as
the whole turn, so its streamed deltas stayed buffered and were stitched
onto the next turn's partial text if the process then died mid-stream —
verified: the new test yields "Checking the logs now.The retry loop"
before the fix. The reset now happens on every assistant.message, since
that event is the turn boundary regardless of whether it carries text.

Verified: go build ./..., go vet, full ./pkg/agent and ./internal/daemon/...
suites pass locally.

Co-authored-by: multica-agent <github@multica.ai>

* fix(channel): tighten the no-narration rule to one sentence

Same contract, fewer tokens: the four-line rationale comment collapses to
one, and the delivery rule drops the restatement, the second example and
the completion examples. What survives is exactly the semantic boundary
the tests pin — no planned/in-progress narration, completed actions still
count as the outcome — plus the one narration example actually observed in
the report.

Verified: go build ./..., go vet, ./internal/daemon/... suite pass locally.
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-28 14:28:38 +08:00
Bohan Jiang
3708fb0f07 fix(daemon): inactivity-based agent run timeout, no wall-clock guillotine (MUL-3064)
Active long-running sessions are no longer killed by a fixed wall-clock deadline. Liveness is delegated to the idle watchdog (MULTICA_AGENT_IDLE_WATCHDOG, default 30m) with a larger in-flight-tool budget (MULTICA_AGENT_TOOL_WATCHDOG, default 2h). MULTICA_AGENT_TIMEOUT is an opt-in absolute cap (default 0 = no cap). The server-side 2.5h sweeper is unchanged as a coarse backstop.

Fixes #3745.
2026-06-05 15:06:07 +08:00
Jan De Dobbeleer
1e1a4f7845 fix(daemon): fix Copilot CLI invocation on Windows and strip shell quotes from custom args (MUL-2876)
Bug 1: detect copilot.cmd/.bat on Windows and invoke the sibling .ps1 directly via powershell -File, bypassing cmd.exe %* re-tokenisation that mangled the multi-line -p prompt. Shared rewriteCmdToPS1() now serves cursor, pi, and copilot.

Bug 2: filterCustomArgs (shared by all agent backends) strips one outer layer of shell quotes via unshellQuoteArg() before processing, so shell-style custom args like --deny-tool='write' no longer reach the CLI with literal quotes.
2026-06-01 23:28:51 +08:00
Multica Eve
72e89a74f3 fix: surface copilot failure details (#2396)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-11 14:08:33 +08:00
Kagura
6d9ca9de93 fix(daemon): suppress agent terminal windows on Windows (#1474)
* fix(daemon): suppress agent terminal windows on Windows (#1471)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add hideAgentWindow to detectCLIVersion and avoid SysProcAttr overwrite

- Add missing hideAgentWindow(cmd) call in detectCLIVersion (claude.go:554)
  so --version checks don't flash console windows on Windows.
- Refactor hideAgentWindow to preserve existing SysProcAttr fields
  instead of overwriting the entire struct.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 17:23:00 +08:00
Bohan Jiang
4ac43e9e49 feat(daemon): log agent invocation at info level (#1428)
Surface the actual exec path + argv for every agent backend at INFO
so operators can see the exact command without flipping to debug.
Also add the missing log line in pi.go for consistency with the
other nine backends.
2026-04-21 14:30:07 +08:00
devv-eve
5fa1da448f fix(chat): preserve chat session resume pointer across failures (#1360)
* fix(chat): preserve chat session resume pointer across failures

The chat 'forgets earlier messages' bug came from PriorSessionID being
silently lost in several edge cases:

- UpdateChatSessionSession unconditionally overwrote chat_session.session_id,
  so any task that completed without a session_id (early agent crash,
  missing result) wiped the resume pointer to NULL.
- CompleteAgentTask + UpdateChatSessionSession ran in separate calls. A
  follow-up chat message claimed in between resumed against a stale (or
  NULL) session and started over.
- FailAgentTask never wrote session_id back, so a task that established
  a real session before failing lost its resume pointer.
- ClaimTaskByRuntime only trusted chat_session.session_id and never
  fell back to the existing GetLastChatTaskSession query, so a single
  bad turn could permanently drop the conversation memory.

This change:

- Use COALESCE in UpdateChatSessionSession so empty inputs preserve the
  existing pointer; surface DB errors instead of swallowing them.
- Run CompleteAgentTask/FailAgentTask + UpdateChatSessionSession inside
  the same transaction (TaskService now takes a TxStarter).
- Extend FailAgentTask + the daemon FailTask path (client, handler,
  service) to forward session_id/work_dir, so failed/blocked tasks that
  built a real session still record it.
- Fall back to GetLastChatTaskSession in ClaimTaskByRuntime when the
  chat_session pointer is missing, and include failed tasks in that
  lookup so a single failure can't lose the conversation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(daemon): forward session_id/work_dir on blocked + timeout paths

runTask previously dropped result.SessionID and env.WorkDir on the
non-completed return paths:

- timeout returned a naked error, so handleTask called FailTask with
  empty session info and the chat resume pointer was either left stale
  or eventually overwritten with NULL.
- blocked / failed (default branch) returned a TaskResult without
  SessionID / WorkDir, so even though FailTask now COALESCEs into
  chat_session, there was no value to write through.
- the empty-output completion path was the same: it raised an error
  even when a real session_id had been built.

All three paths now return a TaskResult that carries the SessionID /
WorkDir the backend produced. Combined with the COALESCE-based update
in UpdateChatSessionSession and the FailTask plumbing introduced in
PR #1360, the next chat turn can always resume from the latest agent
session — even when the previous turn timed out, was rate-limited, or
returned an empty completion — instead of starting over with no memory
of the conversation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(copilot): capture session id from session.start as fallback

The Copilot backend only read sessionId from the synthetic 'result'
event, ignoring the one already present on session.start. When the CLI
was killed before result arrived (timeout, cancel, crash, or a
session.error mid-turn), the daemon reported SessionID="" and the
chat-session resume pointer could not advance — causing the chat to
silently drop conversation memory on the next turn.

Capture session.start.sessionId into state up front, and only let
'result' overwrite it when it actually carries one. result still wins
when present (it is the authoritative end-of-turn record).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(copilot): parse premiumRequests as float to preserve session id

Copilot CLI v1.0.32 serializes premiumRequests as a float (e.g. 7.5),
not an integer. Our copilotResultUsage struct typed it as int, which
made the entire 'result' line fail json.Unmarshal — silently dropping
sessionId on every turn.

This was the real cause of chat memory loss: the daemon reported
SessionID="" to the server, chat_session.session_id stayed NULL, and
the next chat turn never received --resume <id>, so each turn started
a fresh Copilot session with no prior context.

Add a regression test using the real JSON line from CLI v1.0.32 that
asserts sessionId is preserved when premiumRequests is fractional.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: yushen <ldnvnbl@gmail.com>
2026-04-19 22:50:33 -07:00
LinYushen
cd50c31201 feat(agent): add GitHub Copilot CLI backend (#1157)
* feat(agent): add GitHub Copilot CLI backend

Integrate Copilot CLI as a new agent backend using the stable
`-p` JSONL mode (`--output-format json`), following the same
spawn-CLI-scan-JSONL pattern established by claude.go.

Backend (server/pkg/agent/copilot.go):
- Spawn `copilot -p <prompt> --output-format json --allow-all-tools --no-ask-user`
- Parse streaming JSONL events (system/assistant/user/result/log)
- Extract session ID for resume support (`--resume <id>`)
- Accumulate per-model token usage for billing
- Filter blocked args to prevent protocol-critical flag overrides

Daemon config:
- Probe MULTICA_COPILOT_PATH / MULTICA_COPILOT_MODEL env vars
- Copilot uses AGENTS.md (native discovery) and default skills path

Frontend:
- Add Copilot logo SVG and provider switch case

Tests: 14 unit tests covering arg building, event parsing, usage
accumulation, and edge cases. All Go + TS checks pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(daemon): add restart subcommand, make daemon uses it

- `daemon start` keeps original behavior: errors if already running
- `daemon restart` stops existing daemon then starts fresh
- `make daemon` now runs `daemon restart --profile local`

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(copilot): address review nits 1-5

- Nit 1: Add MinVersions["copilot"] = "1.0.0"
- Nit 2: Seed activeModel from session.start.data.selectedModel (falls
  back to opts.Model, then "copilot"). First-turn tokens now get correct
  model attribution.
- Nit 3: Handle assistant.reasoning/reasoning_delta → MessageThinking,
  reasoningText in assistant.message → MessageThinking,
  session.warning → MessageLog{warn}
- Nit 4: Extract handleCopilotEvent() method shared by production and
  tests — no more duplicated switch body that can drift
- Nit 5: Deltas write to output buffer as defense-in-depth; if process
  dies before assistant.message, output is non-empty

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 17:14:56 +08:00