mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-08 06:45:55 +02:00
v0.2.28
225 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eb067ff077 |
fix(server): aggregate task_usage into daily rollup table to cut DB load (#2256)
* fix(server): aggregate task_usage into daily rollup table to cut DB load ListRuntimeUsage previously did a SUM(...) GROUP BY DATE(created_at), provider, model over the raw task_usage stream once per runtime row on the runtimes list and once per detail page load, scaling O(events) per call. This is the hot read path responsible for sustained load on Postgres. Switch the read path to a materialized daily rollup table maintained by a pg_cron job: - 072_task_usage_daily_rollup: schema for task_usage_daily + task_usage_rollup_state, plus rollup_task_usage_daily_window(p_from, p_to) (window primitive used by both cron and offline backfill, idempotent via ON CONFLICT DO UPDATE adding deltas) and rollup_task_usage_daily() (cron entry point — pg_try_advisory_lock(4242) for serialization, watermark advancement, 5-minute safety lag for late-visible inserts). Also adds idx_task_usage_created_at to help the two lazy endpoints (ListRuntimeUsageByAgent / GetRuntimeUsageByHour) that still hit the raw table. - 073_task_usage_daily_pgcron: CREATE EXTENSION IF NOT EXISTS pg_cron in a DO/EXCEPTION block (mirrors the migration 032 pg_bigm pattern so envs without shared_preload_libraries=pg_cron skip gracefully) and schedules rollup_task_usage_daily() every 5 minutes when the extension is present. - queries/runtime_usage.sql ListRuntimeUsage rewritten to read from task_usage_daily; sqlc regenerated. Other usage queries unchanged. - cmd/backfill_task_usage_daily: one-shot Go command that walks task_usage in monthly slices through rollup_task_usage_daily_window, then stamps the watermark to now()-5m so the cron resumes cleanly. Run once after migrations have applied, before relying on the rollup. - runtime_test.go: TestGetRuntimeUsage_BucketsByUsageTime now invokes rollup_task_usage_daily_window after fixture inserts so the handler sees the rolled-up rows. Synthetic daily rows cleaned up after each test. - runtime_rollup_test.go: new tests covering aggregation correctness, idempotency contract of ON CONFLICT DO UPDATE, and the watermark advancing exactly to now()-5m via the cron entry point. Deployment order: apply migrations → run backfill_task_usage_daily once → pg_cron picks up subsequent windows automatically. Today bucket may be up to ~10 minutes stale (5 min cron + 5 min lag) by design. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): make task_usage_daily rollup safe to overlap, replay, and correct Addresses 4 review blockers on the original PR: 1. Cron/backfill double-count race: the rollup function is now idempotent. Window calls find DIRTY KEYS via task_usage.updated_at, then RECOMPUTE each bucket from ground truth and REPLACE the daily row (no more additive ON CONFLICT). Cron and backfill can now overlap safely. 2. Silent pg_cron absence: the read path is gated behind a new USAGE_DAILY_ROLLUP_ENABLED feature flag (default off). The raw task_usage scan is preserved as the fallback. Operators flip the flag per-environment after backfill + cron are confirmed healthy (task_usage_rollup_lag_seconds() helper added for monitoring). 3. UpsertTaskUsage corrections invisible to rollup: added task_usage.updated_at column (default now(), backfilled from created_at), and bumped it on conflict. Corrections now mark the bucket dirty and the next window call recomputes it correctly. 4. CREATE INDEX blocking writes on hot table: split into separate single-statement migrations using CREATE INDEX CONCURRENTLY (074, 075), matching the 035/067 pattern. Also: cron.schedule() removed from migrations entirely. Migration 076 only enables the extension (gracefully on unsupported envs); the actual schedule is a documented operator runbook step that runs AFTER backfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): trigger-driven invalidation + online-safe migration for task_usage_daily Round-2 review feedback on PR #2256: 1. Add explicit dirty-bucket queue (task_usage_daily_dirty) populated by triggers on agent_task_queue (UPDATE OF runtime_id, DELETE) and task_usage (DELETE). The rollup window function drains both this queue and the updated_at-based discovery, so runtime reassignment and issue-cascade deletes no longer leave the rollup divergent from the raw query. Triggers join via agent (not issue) to look up workspace_id, because when the cascade comes from issue, the issue row is already gone by the time atq's BEFORE DELETE fires; agent stays alive. 2. Make migration 072 online-safe: only ADD COLUMN updated_at TIMESTAMPTZ (nullable, no default → metadata-only ALTER, no row rewrite) and a separate ALTER for SET DEFAULT now() (also metadata-only). No bulk UPDATE on the hot task_usage table. The rollup window function's dirty_keys CTE handles legacy NULL rows via an OR branch, supported by partial index idx_task_usage_created_at_legacy. 3. Refresh stale documentation in cmd/backfill_task_usage_daily/main.go header to describe the current recompute/replace semantics, idempotent re-runnability, and the actual migration numbering (072..077). Tests: - TestRollupTaskUsageDaily_InvalidationOnReassign: verifies usage moves between runtime buckets after ReassignTasksToRuntime-style update. - TestRollupTaskUsageDaily_InvalidationOnIssueDelete: verifies daily bucket is cleared after issue delete cascades through atq → task_usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): close dirty-queue race + move legacy partial index to its own concurrent migration Round-3 review feedback on PR #2256: 1. Blocker: dirty-queue invalidations could be silently lost under concurrency. ON CONFLICT DO NOTHING let a late trigger see the row already enqueued, no-op, and then the rollup drain (WHERE enqueued_at < p_to) would delete the original row — losing the late invalidation. Switched all three trigger enqueue paths to ON CONFLICT DO UPDATE SET enqueued_at = GREATEST(existing, EXCLUDED.enqueued_at), so any invalidation arriving during a rollup tick keeps enqueued_at > p_to (p_to = now() - 5min) and survives the post-tick drain. 2. High: idx_task_usage_created_at_legacy (partial index on hot task_usage table) was being created in the regular 077 migration without CONCURRENTLY. Moved to new migration 078 with CREATE INDEX CONCURRENTLY, matching the pattern of 074/075. 077's down migration leaves the index alone (it is owned by 078). 3. Minor: gofmt -w on runtime_rollup_test.go and backfill_task_usage_daily/main.go (tabs were lost in the original heredoc append). PR description rewritten to describe the current recompute/replace + dirty queue + feature flag design and the 072..078 migration ordering. Tests still green: TestRollupTaskUsageDaily_* (including both new invalidation regressions), TestGetRuntimeUsage_*, TestWorkspaceUsage_*. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> * fix(server): unify workspace_id source via agent in rollup window function Round-4 review feedback (J) on PR #2256: M1 (must-fix): The dirty queue triggers resolved workspace_id via `agent.workspace_id`, but the window function's `dirty_from_updates` discovery and `recomputed` recompute join used `issue.workspace_id`. There is no schema-level FK guaranteeing `agent.workspace_id == issue.workspace_id`. Any divergence (future cross-workspace task scenarios, data repairs, migration bugs) would cause: - dirty queue rows with workspace_id from agent - recompute join filtering by workspace_id from issue - 0 matches in recompute → bucket erroneously hits the deleted_empty branch and the daily row is silently dropped - dirty_from_updates path attributing usage to the wrong workspace Replaced both CTEs to JOIN agent (not issue) so trigger / discovery / recompute share one workspace_id source. Comment in 077 explains the constraint. N1: Refreshed two stale references in cmd/backfill_task_usage_daily/main.go (header now says "072..078"; stampWatermark warning now mentions migration 073, where the rollup state table is actually introduced). Test: New TestRollupTaskUsageDaily_WorkspaceMismatch constructs an atq with agent.workspace_id != issue.workspace_id, asserts the bucket lands under agent's workspace (not issue's), and re-asserts after a runtime reassign in the foreign workspace. Acts as a canary if the schema invariant changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Devv <devv@Devvs-Mac-mini.local> |
||
|
|
cc527c34be |
perf(heartbeat): batch runtime last_seen_at writes (#2213)
Batches runtime heartbeat last_seen_at updates while preserving the 60s flush / 150s sweeper stale-window invariant. Also drains pending heartbeat writes during graceful shutdown. |
||
|
|
250ada1fb3 |
chore(db): drop unused agent_task_queue.last_heartbeat_at (#2212)
Drops the unused agent_task_queue.last_heartbeat_at column and removes the hot-path task heartbeat write. |
||
|
|
0af67c8159 |
fix(agent/openclaw): block tasks if openclaw < 2026.5.5 with upgrade hint (#2181)
PR #2101 swapped the openclaw runtime adapter from reading --json on stderr to stdout. That fixed openclaw 2026.5+ but inverted the breakage for pre-2026.5 builds — those still write JSON to stderr, so the adapter now sees an empty stdout and falls through to the same "openclaw returned no parseable output" failure that 2026.5+ users saw before #2101. Add a per-task version gate inside openclawBackend.Execute that runs `openclaw --version`, parses the dotted version, and rejects anything below 2026.5.5 with a hardcoded upgrade hint: openclaw <detected> is below the minimum supported version 2026.5.5. Run `openclaw update` to upgrade and try again. The check is intentionally per-task and uncached so users who upgrade do not need to restart the daemon — the next task automatically re-checks. ~20ms per task is negligible vs. the typical run. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
af971e1e5c |
fix(agent/openclaw): read --json from stdout, not stderr (#2101)
Multica's openclaw runtime adapter has been reading agent output from
stderr since the early openclaw integration days. Current openclaw
(2026.5.5, c37871e) writes its --json blob exclusively to stdout:
$ openclaw agent --local --json --agent main --message 'say hi' >stdout 2>stderr
STDOUT bytes: 27401
STDERR bytes: 0
Result: every successful turn was followed by a daemon-generated system
comment 'openclaw returned no parseable output', visible to users,
looked like the agent broke when it didn't. Reproduced live on WOR-2,
turn at 2026-05-05 16:35 UTC; daemon log confirmed the full result JSON
arrived on the [openclaw:stdout] debug channel and was discarded while
the empty stderr pipe hit the no-events fallback.
Changes
- server/pkg/agent/openclaw.go: swap pipes, StdoutPipe() for the JSON
stream, cmd.Stderr = newLogWriter(...) for log overflow. Cleanup
goroutine now closes stdout on cancel. Comments and the read-error
errMsg updated to reflect the new pipe.
- server/pkg/agent/openclaw_test.go: TestOpenclawProcessOutputReadError
asserts on 'read stdout' (was 'read stderr'), string-only fix,
no behavior change. New TestOpenclawProcessOutputStdoutFixture feeds
a recorded openclaw 2026.5.5 --json blob through processOutput and
asserts result + messages parse cleanly.
- server/pkg/agent/testdata/openclaw-2026.5.5-stdout.json: 27401-byte
fixture captured fresh from the openclaw CLI for the regression test.
Side effects (net positive)
- Log lines openclaw writes to stderr (security warnings, tool errors)
now show up under [openclaw:stderr] instead of being silently consumed
by the JSON parser.
- Daemon's success_pattern heuristic (empty-output -> 'blocked')
becomes meaningful again because result.Output actually populates.
Closes WOR-10.
|
||
|
|
d0ac67dea2 |
fix(skills): drop SKILL.md content from list endpoints (#2180)
* fix(skills): drop SKILL.md content from list endpoints (#2174) `GET /api/skills` and `GET /api/agents/{id}/skills` were SELECT *'ing the skill row and shipping the full SKILL.md `content` blob to every caller. SKILL.md bodies routinely run 50–200KB each, so a workspace with 30–40 skills returned multi-megabyte JSON arrays — past the CLI's 15s timeout on high-latency links and locking out non-US users entirely. Add `ListSkillSummariesByWorkspace` / `ListAgentSkillSummaries` sqlc queries that omit `content`, plus a dedicated `SkillSummaryResponse` wire shape so the contract is explicit (versus stuffing `Content: ""` back into the existing struct). Detail endpoints (`GET /api/skills/{id}`, agent CRUD return values) keep returning the full body. `AgentResponse.skills` and the matching TS `Agent.skills` now use `SkillSummary[]` — frontend list/columns code already only read id/name/description/config.origin, so the type narrowing matches actual usage and prevents new code from accidentally depending on a content field that won't be there. Co-authored-by: multica-agent <github@multica.ai> * fix(agents): narrow embedded skills to AgentSkillSummary; gofmt agent.go GPT-Boy review of #2180: the previous commit typed AgentResponse.Skills as []SkillSummaryResponse, but the agent list batch query (ListAgentSkillsByWorkspace) only joins agent_id/id/name/description, so the wider type left workspace_id/config/created_at/updated_at as zero values. Define a dedicated AgentSkillSummary {id,name,description} that matches what the batch query actually returns and what the frontend actually reads (`agent.skills.map(s => s.name|s.id)`); the standalone GET /api/agents/{id}/skills endpoint keeps SkillSummaryResponse for callers that need the source/origin info. Switch GetAgent's per-agent skills load from ListAgentSkills (full Skill rows including content) back to ListAgentSkillSummaries to avoid reading SKILL.md bodies just to discard them. Re-run gofmt on agent.go to fix the field-tag alignment that drifted when Skills changed type. Co-authored-by: multica-agent <github@multica.ai> * docs(types): correct SkillSummary JSDoc — Agent.skills is AgentSkillSummary[] GPT-Boy spotted on review: comment said SkillSummary was "embedded in Agent.skills", but that field is now AgentSkillSummary[]. Re-point the reader at the right type to avoid future confusion. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
38f777d0ba |
feat(autopilot): auto-pause autopilots with sustained high failure rate (#2136)
* feat(autopilot): auto-pause autopilots with sustained high failure rate Adds a background monitor that pauses any active autopilot whose recent runs are dominated by failures (defaults: ≥100 terminal runs in 7d, ≥90% failed). The monitor leaves a severity=attention inbox notification for the autopilot's creator (or the agent's owner if the autopilot was agent-created) so a human learns about the auto-pause and can fix the root cause before re-enabling. Motivated by MUL-1336 §6 #2: a single broken cron autopilot (`Registro de ls cada 5 min`, 1,475/1,476 failed in 7d) was burning ~1.5k tasks/tokens per week with no human in the loop. Tunable via AUTOPILOT_FAIL_MONITOR_{INTERVAL,LOOKBACK,MIN_RUNS,FAIL_RATIO,STARTUP_DELAY}; INTERVAL=0 disables the monitor entirely. Co-authored-by: multica-agent <github@multica.ai> * chore(autopilot): relax failure monitor defaults to daily / 50 runs Per review feedback in MUL-1339: 30-min scan was overkill — the 50-run threshold already provides multi-hour lag, and operational simplicity matters. Lowering MinRuns from 100 → 50 keeps low-frequency autopilots in scope (~7 runs/day reaches threshold within 7d window). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ba147708a6 |
fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) (#2128)
* fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) Opening an issue from Inbox with thousands of timeline entries used to hard-freeze the browser tab on a synchronous render of every comment + activity. The whole pipeline was unbounded: the API returned every row, TanStack Query cached the full array, and IssueDetail mounted N CommentCards (each running a full react-markdown + lowlight pipeline) in one frame. This swaps the timeline endpoint to keyset cursor pagination and rewires the frontend to useInfiniteQuery so a long issue costs the same as a short one on first paint. API: - GET /issues/:id/timeline now accepts ?before / ?after / ?around (mutex) + ?limit (default 50, max 100); response wraps entries with next/prev cursors and has_more flags. Cursors are opaque base64 (created_at, id). - ?around=<entry_id> anchors a window on the target so Inbox notifications pointing at an old comment never trigger the freeze. - New composite indexes on (issue_id, created_at DESC, id DESC) replace the redundant single-column ones so keyset queries are index-only scans. - /issues/:id/comments default branch now caps at 50 instead of returning every row unbounded; the unbounded ListComments / ListActivities sqlc queries are deleted. Frontend: - useIssueTimeline switches to useInfiniteQuery, exposes fetchOlder/fetchNewer/jumpToLatest + isAtLatest + newEntriesBelowCount. - WS handlers respect the at-latest invariant: comment/activity:created prepends to pages[0] only when the user is reading the live tail; otherwise it just bumps a counter so the UI offers a "Jump to latest" affordance without yanking scroll. - Optimistic mutations adapted to the InfiniteData shape via shared helpers (mapAllEntries / filterAllEntries / prependToLatestPage in core/issues/timeline-cache.ts) and use setQueriesData so all open windows of the same issue stay in sync. - IssueDetail Activity section gets a TimelineSkeleton placeholder during the brief load window plus subtle text-link load-more buttons matching the existing Subscribe affordance (no Button chrome). Top uses a divider for boundary clarity; bottom shows "Jump to latest · N new" weighted slightly heavier when there's unread state. - highlightCommentId now flows into the hook's around parameter so Inbox jumps fetch the surrounding 50 entries directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(agent): default comment list to 50 + prompt hint about long issues The CLI's "multica issue comment list" used to default to --limit 0 (meaning "fetch every comment"), which lets an agent on a long issue fill its context window with thousands of rows. The default is now 50; agents that need older history can pass --limit or --since explicitly. The local-coding-agent prompt also gains a single-line note about this in both the comment-triggered and on-assign flows so the agent knows to scope its fetches when issue size is unknown. Autopilot run-only mode is intentionally unchanged — it has no issue context to query. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3447764b03 |
feat(i18n): full rollout — 21 namespaces translated (en + zh-Hans) (#1853)
* feat(i18n): rollout phase — translate 9 namespaces (WIP)
Phase 1 complete (基建 + login + Settings language switcher),
phase 2 partial (Wave 4 done, search done). Pending namespaces
documented inline; another developer can pick up from here.
Infrastructure
--------------
- server: add users.language column + extend PATCH /api/me
(TestUpdateMeAcceptsLanguage / TestUpdateMePreservesLanguage)
- packages/core/i18n: types / pickLocale (intl-localematcher) /
browser-cookie-adapter / createI18n (initAsync false +
useSuspense false) / I18nProvider / LocaleAdapterProvider
- Split server-safe vs React entries:
@multica/core/i18n — for proxy/RSC/middleware (no React)
@multica/core/i18n/react — for client trees (createContext)
(RSC vendored React lacks createContext; mixed import would crash
proxy.ts at module load.)
- packages/views/i18n: useT hook + selector API augmentation
(i18next v26 default; auto-propagates to apps via the side-effect
import in use-t.ts).
- apps/web: proxy.ts (Next 16 renamed middleware) merges existing
legacy/root redirects with x-multica-locale header forwarding;
layout.tsx reads locale via headers() and pre-loads RSC resources.
- apps/desktop: webPreferences.additionalArguments injects
systemLocale (no sendSync — avoids main-thread blocking IPC);
renderer adapter reads via process.argv.
- ESLint: i18next/no-literal-string at file-scope for translated
files via packages/views/eslint.config.mjs TRANSLATED_FILES.
- glossary.md (packages/views/locales/) freezes term policy:
Issue / Workspace / Agent / Skill / Autopilot / Daemon / Runtime
stay English; Inbox / Project / Comment / Member translate.
Translated namespaces (9 / 19)
------------------------------
- auth: login page (web wrapper含 desktop-handoff 文案) + Settings
Appearance language switcher
- editor: 9 .tsx (bubble-menu / link-hover-card / readonly-content /
title-editor / extensions: code-block / file-card / image-view /
mention-suggestion) + 32 keys
- invite: 25 keys
- labels / members / my-issues: Wave 4 全部
- search: command palette 35 keys
- navigation: no user-facing strings (no-op)
Pending (10 / 19)
-----------------
issues (46 files / ~210 keys)
agents (29 files / ~155 keys; presence.ts + config.ts label maps
允许进 i18n)
onboarding (22 files / ~150 keys)
settings rest / skills / modals / workspace / chat / inbox /
projects / autopilots / layout
Workflow for picking up
-----------------------
- Glossary: packages/views/locales/glossary.md (mandatory read)
- Reference impls: auth/login-page.tsx + editor/* (selector API +
i18n-provider test wrapper pattern)
- Per namespace:
1. create locales/{en,zh-Hans}/{ns}.json
2. add to packages/views/i18n/resources-types.ts
3. useT('{ns}') + t($ => $.foo) in components
4. add files to TRANSLATED_FILES in eslint.config.mjs
5. typecheck + test + lint must pass
- Subagents currently CANNOT write files (sandbox deny). Run as
hybrid: subagent researches + outputs full JSON + tsx diff,
controller writes.
Other
-----
- scripts/init-worktree-env.sh: default
MULTICA_DEV_VERIFICATION_CODE=888888 in dev for deterministic
login (gated by isProductionEnv).
Verified: pnpm typecheck (6 pkgs ok), pnpm test (232 pass),
make test (Go).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(i18n): rewrite glossary aligned with docs zh voice
Switch translation policy to match the canonical CN voice already
established in apps/docs/content/docs/*.zh.mdx (20+ files). The new
rule splits product nouns into two classes:
- Typed entities (issue / project / skill / autopilot / task) — kept as
lowercase English in CN text, visually marking them as system types.
- Concepts (workspace / agent / daemon / runtime / inbox) — fully
translated (工作区 / 智能体 / 守护进程 / 运行时 / 收件箱).
Previous glossary kept Workspace / Agent / Daemon / Runtime as English
on "工程惯例" grounds, but docs zh and CN AI ecosystem (Coze / 腾讯元器
/ 百度) consistently translate these. App UI now matches docs voice so
users don't see split personality between the app and its own docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): register 6 namespaces and retrofit zh strings to new glossary
Two fixes that were blocking the previously-translated namespaces from
actually rendering in CN:
1. RESOURCES gap — locales/index.ts only loaded common/auth/settings,
but resources-types.ts declared 12 namespaces and 6 of them had real
translation content. At runtime i18next would fall back to raw keys
for editor / invite / labels / members / my-issues / search.
Register all 9 currently-translated namespaces.
2. Retrofit zh strings to the docs-aligned glossary:
- "Issue" → "issue" (lowercase entity)
- "Workspace" → "工作区"
- "Agent" → "智能体"
- "Runtime" → "运行时"
- "Skill" → "skill" (lowercase)
- "项目" → "project" (lowercase)
Touched: editor.json (sub_issue + mention.group_issues), invite.json
(3 Workspace occurrences), members.json (agents_section / more_agents),
my-issues.json (8 retrofits across page/header/errors), search.json
(13 retrofits across groups/pages/commands/empty).
Verified: pnpm typecheck (6/6) + pnpm test (238/238) all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate inbox namespace
First namespace through the sub-agent → main-agent integration pipeline.
JSON: en/inbox.json + zh-Hans/inbox.json — 60 keys across page / menu /
list / detail / types / labels / errors. Time-formatter labels are kept
compact in EN ("5m" / "3h" / "2d") and use full units in zh ("5 分钟" /
"5 小时" / "5 天") since raw "5 分" reads as "5 marks/points" in CN.
Component changes converted two module-level statics into hooks so the
strings can flow through i18next:
- inbox-list-item.tsx: `timeAgo` (pure fn) → `useTimeAgo` (hook
returning a fn). The local copy is a duplicate of @multica/core/utils
`timeAgo` that is only used by inbox-page; other consumers across
chat/agents/skills/issues stay on the core util for now and will be
translated when their namespaces land.
- inbox-detail-label.tsx: `typeLabels` (static const Record) →
`useTypeLabels` (hook returning the same Record shape). Call sites
keep the existing `typeLabels[type]` access pattern.
inbox-page.tsx now uses both hooks and `useT('inbox')` selector calls
for all hardcoded strings (~24 sites: header / dropdown menu / list
empty state / detail panel / mobile back / quick-create-failed flow /
all error toasts).
Wired up: resources-types.ts, locales/index.ts RESOURCES, ESLint
TRANSLATED_FILES (3 inbox tsx files now lint-protected).
Verified: pnpm typecheck (6/6) + pnpm --filter @multica/views test
(238/238) + ESLint clean on inbox/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate workspace namespace
Translates the three workspace shell views: create-workspace-form,
new-workspace-page, no-access-page. Also fixes the prior-art
no-unescaped-entities lint errors in no-access-page.tsx — the
apostrophes in "doesn't" / "don't" were JSX text literals that move
into JSON values after translation, so the lint rule no longer fires.
Tests wrapped: workspace/create-workspace-form.test.tsx,
workspace/no-access-page.test.tsx, modals/create-workspace.test.tsx
all now wrap render() with <I18nProvider locale="en"> so the en values
in workspace.json drive the rendered text and the existing assertions
continue to match.
Slug constants kept: WORKSPACE_SLUG_FORMAT_ERROR /
WORKSPACE_SLUG_CONFLICT_ERROR exports in workspace/slug.ts are still
imported by onboarding/steps/step-workspace.tsx (out of scope here).
The workspace shell now reads its strings from workspace.json directly.
Multica.ai brand prefix in the slug input affordance is wrapped with
an inline `// eslint-disable-next-line i18next/no-literal-string` per
glossary policy on brand names.
Renamed sign_in_other → sign_in_different to avoid colliding with
i18next's `_other` plural-suffix convention which the selector-API
typings treated as a plural form of `sign_in`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate projects namespace
Translates the projects list page, project detail page, project picker
dropdown, and project chip — all four user-facing surfaces under
packages/views/projects/components/.
New file: projects/components/labels.ts exposes three hooks that
replace the static `.label` field on PROJECT_STATUS_CONFIG /
PROJECT_PRIORITY_CONFIG and the previous module-level
`formatRelativeDate` helper. Core's `.label` stays untouched (it's
still consumed by search and the create-project modal, both
out-of-scope for this namespace) — those will flip when their
respective namespaces translate.
In zh, the "project" entity stays lowercase English per glossary
(`新建 project`, `还没有 project`, `从 project 移除`). Status / priority /
table column labels translate fully.
The cancelled / done / paused etc. status labels duplicate per-
namespace as `projects.status.*` rather than reading from a future
shared status namespace. This matches the auth/inbox/workspace
pattern of self-contained namespaces. If a generic "issue/project
status" pool emerges later, these can collapse.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on projects/ (1 pre-existing warning
about useEffect/sidebarRef dep, unrelated to i18n).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate autopilots namespace
Six tsx files: autopilots-page (list + 6 templates), autopilot-detail-page
(properties / triggers / run history / delete), autopilot-dialog
(create + edit dialog), trigger-config (cron form), and the agent /
timezone pickers.
Hook conversions for module-level helpers that need t():
- summarizeTrigger / describeTrigger → useSummarizeTrigger /
useDescribeTrigger (no external callers, removed the plain exports)
- formatRelativeDate → useFormatRelativeDate (per-component hook)
- formatCountdown → useFormatCountdown (per-component hook)
- TEMPLATES array now keyed by id; titles + summaries pull from
templates/{id}/{title,summary} JSON. Prompts stay raw EN since
they're injected directly into the agent task — translating them
would translate the agent's instructions, not the user's UI.
Status / execution-mode / run-status enums render via t($ => $.status[k])
with k typed against the core type (no separate hook needed).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on autopilots/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate skills namespace
Seven tsx files: skills-page (list + filters + intro banner),
skill-detail-page (the giant — properties + file tree + sidebar +
conflict banner + delete dialog, ~963 lines), create-skill-dialog
(chooser + manual + URL forms), runtime-local-skill-import-panel
(local runtime browse + import), skill-columns, file-tree, file-viewer.
Notable patterns:
- `createSkillColumns` factory → `useSkillColumns` hook so column
headers flow through useT. Column identity changes per render is
fine — DataTable handles it.
- `validateNewFilePath` (pure helper) → `useValidateNewFilePath` hook
so the 5 validation error messages can be translated.
- skill_files / used_by / description_with_agents use i18next plural
keys (`_one` / `_other`) — the type system collapses these into a
single PluralValue access, so call sites use
`t($ => $.foo, { count })` and i18next picks the form.
- Per glossary, "skill" stays lowercase EN in zh ("新建 skill",
"已删除 skill", "未找到该 skill").
Test wrapper: runtime-local-skill-import-panel.test.tsx now wraps
render() with <I18nProvider> so the assertion on /Import to Workspace/i
matches the EN translation.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on skills/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate chat namespace
Translates all 10 chat surfaces: FAB tooltip, input placeholders,
message list (replied-in / failed-after / tools group / show-details
/ tool result preview), session history (header + time-ago labels),
chat window (new-chat / restore / expand / minimize / agent + session
dropdowns / starter prompts / empty states), context-anchor button +
card tooltips, no-agent banner, offline / unstable banner, and the
task-status pill (queued / starting up / thinking / typing + tool
labels: running command / reading files / searching code / making
edits / searching web).
Hook conversions:
- formatTimeAgo (chat-session-history) → useFormatTimeAgo
- ElapsedCaption now takes a typed `variant` ("replied" | "failed")
instead of a free-text `verb` so the i18n key is enumerable
- pickStage (task-status-pill) refactored: pure pickStageKeys returns
StageKey + optional ToolKey; useResolveStage maps to localized labels
Translation policy notes:
- Starter prompts ("List my open tasks by priority", etc.) are user
UI when displayed AND the user's input when clicked — translating
them sends the agent the user's locale-native phrasing, which is
the right UX for a CN user using a CN agent.
- buildAnchorMarkdown (chat-window) stays in English: it's an
agent-bound markdown prefix injected into the outgoing message,
not user-facing UI.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate modals namespace
Translates all 11 modal sources: registry (no UI text), backlog-agent-hint,
set-parent-issue, add-child-issue, delete-issue-confirm, feedback,
issue-picker, create-workspace, create-project, create-issue (manual),
quick-create-issue (agent panel).
Notable patterns:
- create-project re-uses useProjectStatusLabels / useProjectPriorityLabels
hooks from views/projects/components/labels — same translation source
as the projects list / detail, no duplication.
- create-issue.tsx: renamed `toast.custom((t) => ...)` callback param to
`toastId` to avoid shadowing the closure-captured useT() `t` function.
- Test wrapper added to modals/create-issue.test.tsx so the two assertions
on rendered modal text (success toast + Create another) match the EN
bundle. modals/create-workspace.test.tsx was already wrapped (workspace
ns commit).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate settings namespace (rest of tabs)
Builds on the appearance-tab + language switcher already shipped in
Phase 0. Translates the remaining 8 settings surfaces: settings-page
shell (left nav + tab keys), account / profile, notifications-tab
(5 group labels + descriptions), tokens-tab (create / list /
revoke / created dialog), workspace-tab (general fields + danger
zone + leave/delete confirmations), members-tab (invite + role
config + revoke / remove flows), repositories-tab, labs-tab,
delete-workspace-dialog.
Hook conversion: members-tab `roleConfig` static const → `useRoleLabels`
hook returning a Record<MemberRole, {label, description, icon}>. The
icon stays as a typed React component (Crown / Shield / User), so
rendering pattern is unchanged at call sites.
Test wrapper: settings/components/delete-workspace-dialog.test.tsx
now wraps render() with <I18nProvider> (custom render() helper)
because the test asserts on rendered button labels ("Delete workspace",
"Cancel", "Deleting...").
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate runtimes namespace (entry surfaces)
Translates the user-facing runtime list page surfaces:
runtimes-page (header / search / filters / chips / empty / no-matches /
bootstrapping), runtime-detail (topbar + delete dialog + delete toasts),
runtime-detail-page (not-found state), shared.tsx (4-state HealthBadge
labels).
Hook conversion: shared `healthLabel(health)` was a pure module-level
function. Added `useHealthLabel` hook for translated call sites; kept
`healthLabel` as an EN-only fallback for non-component callers (column
factory in runtime-columns).
Deferred:
- runtime-list / runtime-columns (data table column headers + cell
bodies) — large surface, not in the page-load critical path.
- connect-remote-dialog / update-section / usage-section — secondary
flows, English remains acceptable until a focused pass.
- charts/* — primarily numeric tooltips and axes; minimal user-visible
text.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate layout namespace (sidebar nav, help, loader)
Translates the cross-cutting layout chrome:
- 9 sidebar nav labels (inbox / my issues / issues / projects /
autopilots / agents / runtimes / skills / settings) — driven by
labelKey instead of inline strings, resolved via useT at render.
- HelpLauncher dropdown (trigger aria + 3 items: Docs / Change log
/ Feedback)
- WorkspaceLoader (named + unnamed loading states)
- SortablePinItem unpin tooltip
Pattern shift in app-sidebar.tsx: nav arrays carry `labelKey: NavLabelKey`
(typed against the layout JSON) instead of `label: string`. The string
comparison checks (`item.label === "Inbox"`) became cleaner ID-based
checks (`item.key === "inbox"`).
Deferred: deeper sidebar surfaces — workspace switcher dropdown,
"New Issue" CTA, "Pinned" / "Workspace" / "Configure" group labels —
remain English. The 9 nav labels are the ones that read in every
session.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate onboarding namespace (welcome + step header)
Translates the user-first-impression surfaces of the onboarding flow:
- step-welcome.tsx (the wordmark, headline, lede paragraphs, all CTAs:
Download Desktop / Continue on web / Start exploring / I've done
this before, illustration caption)
- step-header.tsx ("Step N of M" counter + matching aria-label)
- onboarding-flow.tsx (skip-onboarding error toast)
Test wrapper added to onboarding/components/step-header.test.tsx —
custom render() helper wraps with <I18nProvider> so the "Step 2 of 5"
assertions match the EN bundle.
Deferred (acceptable English fallback for now): step-questionnaire,
step-workspace, step-runtime-connect, step-platform-fork, step-agent,
step-first-issue, cli-install-instructions, option-card, runtime
aside panels, starter-content-prompt, cloud-waitlist-expand. These
are deeper steps with significant copy that would benefit from a
focused dedicated pass — voice on each is more nuanced (questionnaire
options, runtime install instructions, agent template recommendations).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(i18n): add EN/zh-Hans key parity guard
Schema-level vitest that walks RESOURCES.en and RESOURCES["zh-Hans"]
namespace by namespace and asserts both bundles cover the same key
set. i18next plural rule is normalized before compare (`_one` /
`_other` collapse to a single logical key) so EN's plural pair
matches zh's `_other`-only form.
Catches retrofit drift where a new EN key lands without zh —
previously this would silently fall back to the English string in
production. Cheap to keep green: 39 tests across 21 namespaces in
under a second.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate issues namespace
Translates the entire issues surface — list / board / detail / comments /
sub-issues / activity feed / batch toolbar / pickers / context menu /
backlog-agent hint dialog / labels panel.
Component coverage:
- issues-page (page header, empty state, move-failed toast)
- issues-header (scope tabs, filter dropdowns w/ status/priority/
assignee/creator/project/label, display settings, sort, view toggle)
- issue-detail (page header, breadcrumb, properties / parent issue /
details / token usage sections, sub-issues, activity timeline,
formatActivity for status/priority/assignee/title/due-date changes,
subscribe/subscriber popover)
- comment-card + comment-input + reply-input (delete dialog, edit/save,
copy/edit/delete row, reply count, placeholders, expand/collapse)
- agent-live-card (is-working banner, tool count, stop / transcript)
- execution-log-section (section header, show/hide past runs, trigger
text builder, status labels, cancel-task)
- batch-action-toolbar (selected count, delete dialog with plurals)
- backlog-agent-hint-dialog (full dialog content)
- labels-panel (intro, create form, list, delete dialog)
- pickers (status / priority / assignee / due-date / label / property
search placeholder + no-results)
- issue-actions-menu-items (all dropdown / context menu items)
- use-issue-actions / use-issue-timeline (toast strings)
STATUS_CONFIG / PRIORITY_CONFIG label rendering routed through
$.status[enum] / $.priority[enum] at every call site; the core config
keeps its English fallback for non-i18n consumers but UI never reads
.label directly anymore.
Tests retrofitted: issues-page, issue-detail, and issue-actions-menu
RTL specs now wrap renders in <I18nProvider> with the EN bundle, so
their string assertions match the bundle (not hardcoded literals).
ESLint i18next allow-list extended to 24 issues files. Verified:
pnpm --filter @multica/views typecheck + test (277/277) all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate agents namespace
Translates the agents listing + detail surface and the create/duplicate
flow. Covers the high-frequency surfaces; deeper sub-tab editors
(activity / instructions / skills / env / custom-args bodies, and the
hooks-buggy runtime/model/concurrency pickers) are deferred — they
have their own pre-existing react-hooks rule violations and benefit
from a focused dedicated pass.
Component coverage:
- agents-page (page header w/ tagline + new button, scope segment,
search, sort dropdown, availability chips, archived toolbar, empty
state, no-matches messaging w/ search interpolation, list-load
error)
- agent-detail-page (back link, archived banner, archive dialog,
not-found state, all 4 toast strings)
- agent-detail-inspector (avatar editor, name + description popover,
description dialog, every PropRow label, validation message,
presence badge label sourced from $.availability[enum])
- agent-overview-pane (tab labels, discard-unsaved-changes dialog)
- create-agent-dialog (title / description / labels / placeholders /
duplicate-suffix / runtime filter buttons / runtime status copy)
- agent-row-actions (full dropdown items + cancel-tasks dialog with
pluralized "N running + M queued" summary + archive dialog + 6 toasts)
- agent-columns (every header cell, You / Archived chips, runtime
fallback labels, availability + workload labels via $.availability /
$.workload, activity tooltip body w/ created_today / created_days_ago
/ runs / failed-percent interpolation)
- inspector/skill-attach (Attach trigger label + aria)
availabilityConfig and workloadConfig now keep colors only — the
display label lives in the bundle, sourced via $.availability[enum]
and $.workload[enum] at every call site. Same pattern as
STATUS_CONFIG/PRIORITY_CONFIG in the issues namespace.
ESLint i18next allow-list extended to 8 agents files.
Verified: pnpm --filter @multica/views typecheck + test (277/277)
all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): clear 30 stray EN strings in translated files
Tail of literal strings missed in earlier passes — the ESLint i18next
allow-list flagged them but they slipped through review. Files touched:
- layout/app-sidebar.tsx (10 keys: Workspaces / Pending invitations /
Create workspace / Join / Decline / Log out / New Issue + shortcut /
Pinned / Workspace / Configure)
- runtimes/components/runtime-detail.tsx (Serving header + serving_count
pluralization, no_agents copy, running/queued chips with count
interpolation, Diagnostics header, CLI label, Delete runtime button,
Technical details toggle, last seen interpolation)
- onboarding/steps/step-welcome.tsx (entire WelcomeIllustration mock —
5 cards × actor names + body copy + 3 mention chips + 2 timestamps;
zh translation reads naturally instead of leaving the demo English)
- settings/components/labs-tab.tsx (`Co-authored-by: ...` git trailer
wrapped in {} so linter sees a JS string, not JSX text — magic
identifier git relies on, must not translate)
- settings/components/members-tab.tsx (✓ glyph wrapped in {})
- modals/feedback.tsx (⌘↵ shortcut wrapped in {})
ServingAgentsCard now reads availability/workload labels from
`agents` namespace (cross-namespace useT) so the bundle-truth pattern
holds: presenceConfig keeps colours only, label text comes from the
shared bundle.
Verified: typecheck + 277/277 tests + lint (only the pre-existing
react-hooks rule-of-hooks errors remain, which task #6 addresses).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agents): rules-of-hooks + translate 4 model/runtime pickers
Three pre-existing react-hooks/rules-of-hooks violations + one missing
useMemo dep cleared, then the four pickers wired through useT.
Hook order fixes:
- concurrency-picker: useEffect now runs before the !canEdit early
return. Stale-draft reset still works the same way.
- runtime-picker: useMemo for the filtered list moved above the
!canEdit branch.
- model-dropdown: `models = data?.models ?? []` was minting a fresh
array each render and tripping the deps lint of the downstream
useMemo. Wrap in useMemo so the reference is stable.
Translation coverage:
- concurrency-picker: tooltip ("Concurrency · N max..."), range
helper text, Save button.
- runtime-picker: trigger label fallback ("No runtime"), tooltip
text composed from {{name}} + status, Mine/All filter buttons,
empty-list copy, "owned by {{name}}" + status fragments in row
tooltip, Cloud badge, online/offline aria.
- model-picker: trigger label, tooltip, "Managed by runtime"
fallback, search placeholder, "Discovering models…", default
badge, "No models available", "Use \"X\"" custom-id flow, Clear
button + its title.
- model-dropdown: every label string including the "Select a runtime
first" / "Default (provider)" / "Runtime offline — enter manually"
trigger fallbacks, the supported=false explanation block, discovery
failed badge, all popover items.
ESLint allow-list extended to 4 picker files. Verified: typecheck +
277/277 tests + lint (0 errors, only pre-existing react-hooks warnings
in unrelated files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate runtimes list + connect dialog + CLI updater
Three deep runtime surfaces wired through useT, with the agents
namespace doing double duty for shared availability/workload labels.
runtime-columns:
- 7 column headers via t-augmented createRuntimeColumns({ t }).
- HealthCell now reads from useHealthLabel() (already translation-aware)
instead of the EN-only healthLabel() helper.
- WorkloadCell sources the label from $.workload[enum] (cross-namespace
to agents) — colour stays via workloadConfig.
- CostCell delta "flat" copy + CLI cell "Desktop" badge + update-
available aria/tooltip + RowMenu's full delete dialog (title /
description with {{name}} interpolation / cancel / confirm /
deleting state) plus its admin-permission hint.
connect-remote-dialog:
- Three steps fully translated: instructions (header + 4 numbered
steps + security warning + troubleshooting list with mono code
snippets escaped as JS strings), waiting (loader + hint), success
(CTA pair).
- Mono CLI commands wrapped in {} so linter sees JS strings — those
are literal commands that must stay untranslated for the user to
paste into a terminal.
update-section:
- statusConfig collapsed to icon+colour only; labels move to
$.update.status[enum] for proper translation per-state.
- "CLI Version:" / "Latest" / "available" / "Update" / "Retry"
copy + the "Managed by Desktop" tooltip and disabled hint.
Layout helpers tagged: runtime-list passes `t` through to the column
factory the same way agent-columns does.
ESLint allow-list extended with the 4 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. usage-section.tsx
(KPI cards / WhenChart / TopUsageBreakdown / receipt table) is the
remaining runtimes surface — chart-heavy and benefits from a focused
pass next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate 5 agent detail tabs + skill-add dialog
The 5 tabs that fill the agent detail right pane plus the shared
skill picker dialog. Agents bundle gains a `tab_body` block with
sub-namespaces per tab + a `common` slot for save/add/unsaved.
Tab coverage:
- instructions-tab: intro paragraph, multi-line example placeholder
(full 18-line zh translation), Save / Unsaved.
- env-tab: read-only intro / empty state, editable intro with two
inline `<code>` env-var examples kept English (mono terminal
payloads), KEY / value placeholders, Show/Hide value aria, Add /
Remove aria, all 3 toasts (duplicate keys / saved / save failed).
- custom-args-tab: intro about whitespace splitting, launch-mode
prefix line + `<your args>` placeholder, --flag value placeholder,
Add, Remove aria, both toasts.
- skills-tab: intro, Add skill button, import-hint callout, empty
state title + hint + add-CTA, remove-failed toast.
- activity-tab: 3 section titles (Now / Last 30 days / Recent work),
active-task pluralization, performance subtitle, all 3 empty
states, runs/success%/avg-duration/failed pluralization with
interpolation, source labels (Issue / Chat / Autopilot / Untracked),
source fallbacks (Quick create / Creating issue / Chat session /
Autopilot run), issue-short fallback, "Triggered by" tooltip
header, open-issue / transcript / cancel-task tooltips and ARIAs,
cancelling state, started/dispatched/queued time prefixes, show
more.
- skill-add-dialog: dialog title + description, empty list copy,
Cancel button, add-failed toast.
skills-tab.test.tsx wrapped in <I18nProvider> with the EN bundle so
its `Local runtime skills are always available` assertion still
matches the resolved translation instead of the raw key path.
ESLint allow-list extended with the 6 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. Only the per-test
mock for skills-tab needed wrapping; the other 4 tabs ship without
test files of their own and inherit the I18nProvider chain via
agent-overview-pane / agent-detail-page test renders (when those
exist later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate onboarding step-questionnaire + option-card
The user-profile step (3 questions) is the first deferred onboarding
deep step now wired through useT.
step-questionnaire:
- Eyebrow + headline + answered-progress counter with {{count}}
interpolation
- All 3 questions and their option labels (team size / role / use case)
- All 3 "Other" placeholders for free-text fallback
- Right-rail "Why three questions" / "What you get" panel: 2 eyebrow
rows, 2 unlock-item title+body pairs, learn-more link
- Back / Continue buttons via shared `common` block
option-card: shared "Other" radio label and aria.
Test wrapped in <I18nProvider>. EN value of `other_label` kept as
"Other" so the existing /^other$/i regex in step-questionnaire.test
keeps matching after the rendering pipeline switched from a hardcoded
literal to a bundle lookup.
ESLint allow-list extended with these 2 files. The remaining 4 deep
steps (workspace / runtime-connect / platform-fork / agent), the
2 ancillary surfaces (cli-install-instructions / starter-content-
prompt), and the 3 side panels (runtime-aside-panel / cloud-waitlist-
expand / compact-runtime-row) will be surfaced + swept by the global
ESLint switch (next commit).
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): flip ESLint to glob + drain remaining hardcoded EN
ESLint i18next/no-literal-string now applies to **/*.tsx by default
instead of an explicit allow-list. Files that genuinely still need
hardcoded EN are listed in STILL_HARDCODED — concrete, finite, and
the goal is to drain that list to zero.
Tail strings translated in this commit (surfaced by the global flip):
- common/task-transcript/agent-transcript-dialog.tsx — full dialog:
status badge (Running / Completed / Failed), sr-only DialogTitle,
Filter dropdown trigger + Clear filters, Copy all / Copy filtered /
Copied, tool-calls + events metadata chips with pluralization,
events-filtered "{{shown}} of {{total}}" interpolation, "Waiting
for events..." live state, "No execution data recorded." past
state. New `transcript` block in agents namespace.
- runtimes/components/charts/activity-heatmap.tsx — Less / More
legend labels around the contribution-style heat squares.
- search/search-trigger.tsx — sidebar Search... button label.
⌘ glyph wrapped in {} to satisfy the linter (mono shortcut symbol,
not translatable).
Holdouts (STILL_HARDCODED, ~14 files): the deep onboarding steps
(workspace / runtime-connect / platform-fork / agent / first-issue /
cli-install-instructions, plus 4 ancillary panels), the runtimes
usage-section + KPI cards, and 5 minor agent visual primitives
(sparkline / agent-presence-indicator / agent-profile-card /
visibility-badge / char-counter). Each one gets a dedicated future
pass; the global rule prevents new hardcoded strings from landing
elsewhere.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): drain agent visual primitives + onboarding small components
8 files removed from STILL_HARDCODED:
agents/components/:
- char-counter — over-limit text with {{count}} interpolation
- visibility-badge — uses new agents.visibility.{private,workspace}.
{label,tooltip} block; drops VISIBILITY_LABEL/TOOLTIP imports from
core in favour of bundle-driven copy
- agent-presence-indicator — availability + workload labels via
$.availability[enum] / $.workload[enum] (cross-namespace),
queue-badge "+N queued" with pluralization
- agent-profile-card — Agent unavailable / Detail link / Owner /
Skills / Runtime / Unknown runtime / Archived chip / availability
line via cross-namespace lookup
agents.json: new presence + visibility + profile_card + char_counter
blocks.
onboarding/components/:
- compact-runtime-row — online/offline aria via agents.availability
- runtime-aside-panel — full content (What's a runtime / Good to
know / Swap anytime / Add more later / docs link)
- starter-content-prompt — full dialog (title / description with
inline emphasis / both buttons / 3 toasts)
- cloud-waitlist-expand — intro paragraph + warning span / email
+ reason labels + placeholders + Optional badge / Join + on-list
states / both toasts
onboarding/steps/:
- cli-install-instructions — copy aria + intro + 2 step labels
onboarding.json: new runtime_aside / cli_install / starter_content /
cloud_waitlist blocks.
Tests for step-platform-fork + step-runtime-connect wrapped in
<I18nProvider> with EN bundle so /you're on the list/i etc. still
matches the resolved translations.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate onboarding deep steps
The 5 large onboarding steps that were deferred from earlier passes,
plus their support helpers, all wired through useT.
step-first-issue (final beat — flips onboarded_at):
- error_title / Retry / retry_failed toast / finishing / opening
states.
step-agent (creates the user's first agent):
- Templates moved from a module-level const to a useT-driven
useAgentTemplates() hook. Names + emoji stay constant (visual
identity), labels + blurbs + instructions resolve from the
bundle. coding / planning / writing / assistant — all four
templates ship a full zh translation that reads naturally.
- Recommended badge, eyebrow + headline + lede, footer hint,
Create {{name}} CTA, create_failed toast.
- Right-rail "About agents" panel (4 way-items + headline +
add-more hint + docs link).
step-workspace (create or pick existing):
- 5 footer states (open / creating / creating-pending / name-first
/ pick), all hint + CTA strings via interpolation.
- Name + URL + slug placeholders, issue-prefix preview spans,
Create-new card title + subtitle.
- 8-row WorkspacePreviewCard sidebar (Inbox / Issues / Agents /
Projects / Autopilot / Runtimes / Skills / And more) — every
label + meta strapped to bundle keys.
- 4 perks (assign / chat / invite / switch) + 3 next-steps
(runtime / agent / starter), 2 toasts (slug-conflict / failed).
- `multica.ai/${slug}` mono URL escaped via template-literal
expression so the linter sees a JS string.
step-runtime-connect (desktop scan flow):
- 3 phase headlines + ledes (scanning / found / empty), trust-strip
status (all online / N online / none online) with pluralization,
online/offline labels, Skip / Continue / Selected hint.
- Empty-view 2 cards (skip + waitlist) and the cloud waitlist
dialog wrapper.
step-platform-fork (web fan-out):
- Eyebrow + headline + lede, footer hint with 3 phase variants.
- Primary download card (before/after click) + 2 alt cards (CLI /
cloud) + CLI dialog with 4 elapsed-time stages (normal / midway /
slow / stalled), live-listening header, runtime-connected
pluralization, cloud waitlist dialog.
ESLint: STILL_HARDCODED list shrunk from 14 entries to 1 — only
runtimes/components/usage-section.tsx (chart-heavy KPI panel)
remains.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate runtimes usage panel + drop STILL_HARDCODED
Final i18n holdout: the runtimes usage panel (KPI hero, WHEN chart
tabs, cost-by breakdowns, daily breakdown table) is wired through
useT("runtimes"). With this drained, the eslint scaffolding for
explicit holdouts is removed — every JSX text node in @multica/views
now flows through i18n.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): drain rollout gaps + add cross-device sync
Lands the post-review punch list for the i18n rollout: closes correctness
gaps that would have shipped silently, and adds the missing cross-device
locale sync the rollout's docs already promised.
Coverage:
- Register issues + agents namespaces in RESOURCES (90 useT call sites
were rendering keys-as-text in production)
- Harden parity test to compare RESOURCES keys against on-disk JSON
files, so a future missing namespace registration fails loudly
- Server-side language whitelist in UpdateMe + reject-unsupported test
- Safe SupportedLocale resolution in appearance-tab (no more `as` cast
on a region-tagged BCP-47 string)
- HTML lang attribute uses zh-CN (not zh-Hans) for screen reader / CJK
font-stack compatibility
- Cookie Secure flag on https
- Pulled createBrowserCookieLocaleAdapter out of the server-safe entry
into a new @multica/core/i18n/browser subpath; document.cookie access
can no longer leak into Edge middleware imports
Cross-device sync:
- New UserLocaleSync component mounted in CoreProvider; on login, if
user.language differs from the active i18n.language, persist via the
adapter and reload. Both apps benefit
- Desktop main process tracks system locale and emits IPC on focus when
it changes; renderer reloads only when the user has no explicit
Settings choice (their preference still wins)
Tests:
- pickLocale / matchLocale (11 cases incl. region-tagged BCP-47, malformed
tags, zh-Hant collapse-to-zh-Hans semantics)
- browser-cookie-adapter (6 cases under jsdom)
- Shared renderWithI18n helper at packages/views/test/i18n.tsx that wraps
the real RESOURCES map; future tests opt in instead of inlining a
per-file TEST_RESOURCES slice that goes stale silently
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(conventions): consolidate naming + i18n glossary into docs site
Single source of truth for code naming, i18n translation glossary, and
Chinese voice rules. Previously split between packages/views/locales/glossary.md
and scattered comments — now lives at apps/docs/content/docs/developers/conventions.{mdx,zh.mdx}
with both English and Chinese versions kept in sync.
Three sections per page:
1. Code naming — routes, packages, files, DB, Go, TS, commits
2. i18n translation glossary — entity vs concept rule, what to translate,
word combination, plurals, interpolation, key naming
3. Chinese voice + style — punctuation, principles, where to look in doubt
Side effects:
- packages/views/locales/glossary.md collapses to a stub redirecting to
the docs page; do not edit it
- CLAUDE.md gets a new top-level "Conventions reference" section so any
Claude session sees the pointer before any other rule
- apps/docs/content/docs/developers/ gets a stub English meta.json so the
conventions page is reachable on the EN side (contributing.zh.mdx /
architecture.zh.mdx remain ZH-only — separate work)
- Both root sidebars get a new "Developers" group
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): apply zh voice rules + translate project/autopilot
Two-part cleanup driven by the conventions doc landed last commit:
Voice violations (mechanical sweep across 10 zh-Hans namespaces):
- 「」 (Japanese-style brackets) → \" to match the EN source's straight
double quotes (~13 sites)
- … (single-char ellipsis) → ... three dots (~43 sites)
- Drop translation-ese pronoun "我们" where it's a pure narrator
("我们已发送" → "已发送", "我们替你托管" → "由 Multica 托管"); keep
"告诉我们" where "we" is the legitimate brand recipient
- "作为父级 / 作为子级" → "设为父级 / 设为子级"
- "任务" mistranslated as the task entity → `task` (lowercase EN entity)
- Dialog title "Autopilot" → "autopilot"
Translate project / autopilot per industry consensus:
- `project` → 「项目」 (~42 value sites). Feishu / Tower / Teambition /
PingCode / GitHub Projects all translate; no Chinese product keeps
`project`.
- `autopilot` → 「自动化」 (~34 value sites). Avoids the Tesla-style
「自动驾驶」 association; matches Notion / Feishu's industry term.
- Issue / skill / task remain lowercase EN per dev-team familiarity.
- Sidebar nav-label entities get Title Case ("Issue" / "Skill" / "我的
Issue") so the entry-point label reads as a proper UI signal; body
prose stays lowercase.
Conventions doc (EN + ZH) reflects the decision and adds a "why these
translate but issue/skill/task don't" rationale block.
Verification: parity test 45/45, full monorepo typecheck green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate chat session delete + project resources section
Two features main shipped while this branch was idle never went through
the i18n pass:
- Chat session delete confirmation dialog (#2115) and history toggle
tooltip (#2117): adds session_history.delete_dialog.* and
session_history.row_delete_*, plus window.history_show_tooltip /
history_back_tooltip.
- Project resources sidebar (#1926/#2080/#2111): entire component
including toasts, popover form, attach/remove tooltips. New
projects.resources subtree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
09f04847d3 | feat(server): redis-backed runtime liveness with DB fallback (#2121) | ||
|
|
ee10c508fb |
fix(daemon): trust the agent's session id from session/resume across ACP backends (#2070)
When the local state.db of an ACP backend (hermes, kimi, kiro) is wiped — crash, config change, manual kill, container reset — the backend's session/resume (or session/load, in kiro's case) silently creates a brand-new session rather than failing, and returns the new id in the response. Today the daemon ignores the response and stamps sessionID = opts.ResumeSessionID across all three backends, so every subsequent session/prompt is addressed to a session id the backend has no record of. The task fails with JSON-RPC -32603 (Internal error) on the very first turn, with no operator-visible signal that the problem is a session-id mismatch one layer down. The behavior is invisible: agent shows "started", then "failed" with a generic Internal error. Reproducing in production took repeated runs because nothing in the logs pointed at the silent reset. Fix: route all three ACP backends through a small `resolveResumedSessionID` helper that: - prefers the id the backend returned in its response (the canonical id; the one the backend will accept on the next call) - falls back to the requested id when the response is malformed, empty, or omits sessionId — defensive fallback so older / non- conforming backends (notably kiro's current session/load shape) behave identically to today - signals (via a bool) when the id changed, so the caller logs a Warn with `backend=<hermes|kimi|kiro>` and operators can grep for silent state resets to correlate them with task failures Why this is at the backend layer rather than the daemon's existing session-resume fallback: server/internal/daemon/daemon.go:1554-1566 already retries with a fresh session when resume fails, but it gates on `result.Status == "failed" && result.SessionID == ""`. The backend WILL hand back a result.SessionID — just the new one it silently committed to — so the daemon-level fallback never fires for this failure mode. The helper is also what session/new already uses (extractACPSessionID, documented in code as "Shared by all ACP backends"). session/new extracts the canonical id from the response; session/resume just didn't, until now. Coverage: - hermes.go: confirmed bug, root cause of -32603 in production - kimi.go: same code shape, same protocol method, same response schema as hermes (per extractACPSessionID's comment) — same bug - kiro.go: same code shape, different method (session/load). Current observed response doesn't include sessionId, so the defensive fallback means today's behavior is preserved. Routing through the same helper means a future kiro release that DOES return a sessionId on silent reset works the same way as hermes/kimi without another diff. Tests (server/pkg/agent/hermes_test.go — helper covers all three backends, no per-backend duplication): - TestResolveResumedSessionIDMatching — backend confirms requested id - TestResolveResumedSessionIDDifferent — backend returned a new id; caller is told to switch - TestResolveResumedSessionIDEmptyResponse — older / malformed body; defensive fallback to requested id (covers kiro's current shape) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a4fac51cf5 |
fix(projects): add resource_count breadcrumb instead of inlining resources (#2118)
* fix(projects): add resource_count breadcrumb to project responses Closes #2087 `multica project get` previously returned project metadata with no signal that resources existed. Agents that fetched a project this way had no way to discover its attached resources without already knowing about `/api/projects/{id}/resources` or the on-disk `.multica/project/resources.json`. Rather than inline the full resource list into the parent payload (which conflates parent metadata with a child sub-collection and locks the resource_ref shape into the project endpoint's contract), this adds a scalar `resource_count` breadcrumb to ProjectResponse. The actual list stays at the dedicated sub-collection endpoint. Changes: - GetProjectResourceCounts :many — new batched sqlc query - ProjectResponse.ResourceCount populated in GetProject, ListProjects, SearchProjects, and the with-resources CreateProject echo - multica project get prints a stderr hint pointing at multica project resource list <id> when count > 0; the JSON on stdout stays parseable - Meta-skill (runtime_config.go) lists multica project get and multica project resource list in Available Commands so agents that read CLAUDE.md / AGENTS.md know about both paths Co-authored-by: multica-agent <github@multica.ai> * fix(projects): wire ResourceCount through Update + Create event payload Review feedback on #2118. - UpdateProject now reloads ResourceCount before responding/publishing. Previously a title- or status-only PUT served (and broadcast over WS) resource_count: 0 even when resources existed. - The with-resources CreateProject path sets resp.ResourceCount before the project:created publish, so the WS event payload matches the HTTP echo. The hand-rolled response map collapses to an embedded ProjectResponse + resources array — one source of truth for the serialized shape. - packages/core/types/project.ts: Project gains resource_count: number to keep the TS contract aligned with the server response. Tests: - TestProjectResourceCountBreadcrumb extends to assert UpdateProject preserves the breadcrumb. - TestCreateProjectWithResourcesEchoesCount asserts the create echo carries resource_count matching the attached resources. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
60b215f44f |
feat(chat): support deleting chat sessions (#2115)
* feat(chat): support deleting chat sessions
Replaces the unreachable archive endpoint with a real hard delete and
exposes it from the chat history panel.
- DELETE /api/chat/sessions/{id} now hard-deletes the session and its
messages (CASCADE), cancels any in-flight tasks before removal so the
daemon doesn't keep running work whose result has nowhere to land,
and broadcasts chat:session_deleted.
- Frontend adds a per-row delete button with a confirmation dialog,
optimistically drops the session from both list caches, and clears the
active session pointer locally + on other tabs via the WS handler.
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): make session delete atomic and keep archived sessions read-only
Address review feedback on #2115.
- DeleteChatSession now runs lock + cancel + delete in a single tx and
only broadcasts events post-commit. The new LockChatSessionForDelete
query takes FOR UPDATE on chat_session, which blocks the FK validation
of any concurrent SendChatMessage trying to enqueue a task for this
session — that insert fails after we commit, so it can no longer
produce an orphaned task whose chat_session_id is nulled by
ON DELETE SET NULL. Cancel failure now aborts the delete instead of
warn-and-continue.
- SendChatMessage refuses non-active sessions again. The archive code
path is gone, but legacy rows with status='archived' may still exist
in the DB; keep the guard until we explicitly migrate them.
- Frontend re-reads allChatSessionsOptions to disable ChatInput on
legacy archived sessions so the UX matches the server-side guard.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
1476c268dd |
refactor(quick-create): exempt git-describe daemons from CLI gate (#2108)
* refactor(quick-create): remove daemon CLI version gate Local-source daemons report dev-suffixed versions (e.g. v0.2.15-235-gdaf0e935) that the picker pre-check and server gate both treat as too old, blocking quick-create during local testing. Drops the gate end-to-end: removes MinQuickCreateCLIVersion + CheckMinCLIVersion in pkg/agent, the checkQuickCreateDaemonVersion handler and readRuntimeCLIVersion helper in handler/issue.go, and the mirrored cli-version.ts plus the modal's pre-check, blocked-state UI, and daemon_version_unsupported error branch. Co-authored-by: multica-agent <github@multica.ai> * refactor(quick-create): skip daemon CLI version gate in dev Restores the gate (reverts the full-removal commit) and bypasses it in non-production environments instead. The motivation for the original removal — local source-built daemons report a `git describe` version like v0.2.15-N-gHASH that parses below 0.2.20 and blocks dev testing — is now handled by checking APP_ENV on the server and NODE_ENV on the client. Production keeps the original "needs upgrade" UX. Co-authored-by: multica-agent <github@multica.ai> * refactor(quick-create): exempt git-describe daemons instead of env bypass Replaces the per-environment bypass added in the previous commit with a shared daemon-version signal. CheckMinCLIVersion / checkQuickCreateCliVersion now treat any daemon whose CLI version matches the `vX.Y.Z-N-gHASH[-dirty]` git-describe shape as OK; tagged releases keep going through the normal min-version comparison. Why: Emacs flagged that (a) NODE_ENV !== "production" also disables the gate on staging and other non-prod deployments, undoing the protection for the case the gate was originally written for, and (b) NODE_ENV (web client) and APP_ENV (server) are not equivalent, so the modal pre-check and server gate could disagree on the same request. Both go away when the signal is intrinsic to the daemon's version string. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5c42ed1649 |
fix(server): allow re-inviting after invitation expires (#2059)
The uniqueness check on workspace invitations only filtered by status='pending', not by expires_at. Combined with the partial unique index idx_invitation_unique_pending (also keyed only on status), a past-due pending row permanently blocked re-inviting the same email. Now, before creating a new invitation, the handler flips any past-due pending row for the same (workspace_id, invitee_email) to 'expired', freeing the unique slot. Also tightens GetPendingInvitationByEmail to require expires_at > now(), matching the existing list queries. Closes multica-ai/multica#2055. |
||
|
|
629f4136ac |
fix(codex): handle MCP elicitation server requests correctly (#1944)
* fix(codex): handle MCP elicitation server requests correctly Fixes #1942. handleServerRequest responded with {} to unrecognized Codex server requests including mcpServer/elicitation/request. Codex 0.125+ expects {action, content, _meta} for elicitation — the empty object causes a deserialization error and the MCP tool call is reported as user-rejected. Changes: - Add mcpServer/elicitation/request case with correct response schema - Add respondError helper for JSON-RPC error responses - Return proper JSON-RPC method-not-found error for unknown server requests instead of silent empty object - Add tests for MCP elicitation and unknown method handling * fix: use cfg.Logger instead of global slog in codex handleServerRequest Switch the unhandled-server-request warning from global slog.Warn to c.cfg.Logger.Warn for consistency with all other log calls in codex.go. This ensures the warning appears in daemon run-logs and per-task pipelines where operators look during triage. |
||
|
|
170fa2102b |
fix(agent/hermes): wire streamingCurrentTurn gate to drop history replay (#2024)
Hermes ACP can flush queued session updates from the previous turn before the current turn actually starts — both as session/resume history replay and as chunks queued before our session/prompt response streams. Without a gate those updates were appended to output and re-emitted to the UI, so the previous answer appeared duplicated next to the new one. Closes #1997. PR #1789 added the acceptNotification hook field to hermesClient and the call site in handleNotification, but never assigned it for Hermes, so the guard short-circuited and every notification was processed. This change mirrors the working Kiro pattern (kiro.go:87/97/240): - declare a streamingCurrentTurn atomic.Bool in the backend. - assign acceptNotification, onMessage, onPromptDone gates that all return early when the flag is false. - flip the flag to true immediately before c.request("session/prompt"). Adds TestHermesClientAcceptNotificationGate as a regression test that exercises the gate directly on hermesClient. Verified with `go test ./pkg/agent`. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f046d03f7 |
fix(agent): expose GPT-5.5 family in Codex runtime model picker (#2020)
Latest Codex CLI ships with GPT-5.5 / GPT-5.5 mini, but the static catalog still topped out at GPT-5.4 so users couldn't pick the new model from the agent picker. Add gpt-5.5 + gpt-5.5-mini to codexStaticModels and promote 5.5 as the default badge. Keep the older 5.4 / 5.3-codex / gpt-5 / o3 entries for users on older Codex CLI builds. Add a regression test mirroring TestGeminiStaticModelsExposesAliasesAndGemini3 so the next OpenAI release isn't a silent miss. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cf47d9b702 | fix: guard session resume by runtime (#1905) | ||
|
|
2dddfaa196 |
feat(daemon): Redis empty-claim fast path for /tasks/claim polling (#1860)
* feat(daemon): Redis empty-claim fast path for /tasks/claim polling Daemons poll /tasks/claim every 30s per runtime; the steady-state warm-empty case currently runs ListPendingTasksByRuntime against Postgres on every poll. This collapses that path: - New ListQueuedClaimCandidatesByRuntime query restricts to status = 'queued' (the old query also returned 'dispatched' rows that can never be reclaimed) and is backed by a partial index keyed on (runtime_id, priority DESC, created_at ASC). - New EmptyClaimCache caches the negative verdict in Redis with a 30s TTL. ClaimTaskForRuntime checks the cache before SELECT and populates it on confirmed-empty results. - notifyTaskAvailable now invalidates the runtime's empty key before kicking the daemon WS, so newly enqueued tasks become claimable immediately rather than waiting out the TTL. - AutopilotService.dispatchRunOnly now goes through TaskService.NotifyTaskEnqueued so run_only tasks get the same invalidate-then-wakeup contract as every other enqueue path. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): close MarkEmpty/Bump race in empty-claim fast path GPT-Boy's review on PR #1860 caught a real concurrency bug. Under the prior implementation it was possible for a slow claim to write an empty verdict AFTER a concurrent enqueue had already invalidated it: T1 claim: SELECT -> empty T2 enqueue: INSERT row, DEL empty key (no-op, key not set yet), wakeup T1 claim: SET empty (writes a stale "empty" verdict) T3 wakeup: IsEmpty -> hit -> returns null The just-queued task would then sit idle until the empty key's TTL expired (up to 30s). Replace the DEL-based invalidation with a per-runtime version counter: - CurrentVersion(rt) is a Redis INCR counter at mul:claim:runtime:version:<rt> with a 24h sliding TTL. - Claim samples version BEFORE the SELECT and passes it to MarkEmpty, which stores the verdict's value as the observed-version string. - IsEmpty MGETs both keys and trusts the verdict only when the empty-key value equals the current version. - Enqueue Bumps the version (INCR + EXPIRE) before the wakeup, causing any verdict written under a prior version to be rejected on the next read. Also bound every Redis call from this cache with a 250ms timeout — notifyTaskAvailable uses a background context so a wedged Redis must not block enqueue. Tests against a real Redis (REDIS_TEST_URL) cover: - MarkEmpty + IsEmpty under matching version returns hit - Bump invalidates a prior empty verdict (race-fix pin) - A MarkEmpty written under a stale pre-Bump version is rejected - TTL clamping, per-runtime isolation, nil-cache safety - notifyTaskAvailable Bumps before the wakeup fires Co-authored-by: multica-agent <github@multica.ai> * chore(daemon): renumber claim-candidate index migration to 067 Slot 064 was taken on main by 064_notification_preference. The migration runner tracks per-version in schema_migrations and would silently skip the second 064_*, leaving the index uncreated. Rename to 067 (next free slot). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1345685a3 |
fix(task): rerun starts a fresh session, skip poisoned resume (#1928)
* fix(task): rerun starts a fresh session, skip poisoned resume
When a task ended in a known agent fallback ("I reached the iteration
limit and couldn't generate a summary.", "Put your final update inside
the content string. Keep it concise.") the (agent_id, issue_id) resume
lookup would still pick that session, so a manual rerun inherited the
poisoned state and reproduced the same bad output.
Two complementary guards:
1. Daemon classifies poisoned terminal output and routes it through the
blocked path with failure_reason set ('iteration_limit' /
'agent_fallback_message'). GetLastTaskSession excludes failed tasks
with those reasons, so even comment-triggered tasks no longer resume
them. Tasks that failed mid-flight (timeout, runtime_recovery, etc.)
are still resumable, preserving MUL-1128's auto-retry contract.
2. Manual rerun marks the new task force_fresh_session=true. The daemon
claim handler skips the resume lookup entirely when the flag is set,
capturing the user-intent signal that "the prior output was bad" even
when poisoned classification misses a future fallback wording.
Auto-retry of orphaned mid-flight failures (MaybeRetryFailedTask →
CreateRetryTask) does not take this path, so it keeps resuming.
Tests: classifyPoisonedOutput unit test; integration tests assert the
SQL filter excludes poisoned classifiers, RerunIssue flips the flag,
and the normal enqueue path leaves it false.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): cap poisoned-output matcher to short trimmed text
GPT-Boy review on MUL-1630: the previous strings.Contains match would
classify any output that quoted the marker substring — including a
review/analysis that simply discussed the marker itself. Real fallback
messages are short single-sentence affairs, so cap the candidate at
~one paragraph and trim whitespace before matching. Adds regression
tests covering a long quoting review and a marker buried in a long
real conclusion; both must stay classified as completed.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): rename 065 force_fresh_session → 066 to clear collision
main introduced 065_project_resources after this branch was cut, so
both files shared the 065_ prefix. The readiness check
(server/cmd/server/health.go → migrations.LatestVersion) takes the
last entry by lexical order, which is 065_project_resources, leaving
this branch's 065_force_fresh_session unguarded — a deploy that
applied project_resources but not force_fresh_session would still
report ready, and the next enqueue / rerun / claim would crash on
"column force_fresh_session does not exist".
Renaming to 066_force_fresh_session puts it strictly after
project_resources so readiness blocks until it's applied.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
44608713bb |
feat(projects): typed project resources + agent runtime injection (#1926)
* feat(projects): typed project resources + agent runtime injection
Adds a `project_resource` table that lets a project carry typed pointers
(github_repo today, more later) and surfaces them at agent runtime.
Server
- migration 065: project_resource (resource_type TEXT + resource_ref JSONB)
- sqlc CRUD + handler at /api/projects/{id}/resources
- claim handler attaches project_id/title + resources to issue tasks
Daemon
- TaskContextForEnv carries project context
- writes .multica/project/resources.json into workdir
- adds "## Project Context" block to CLAUDE.md / AGENTS.md / GEMINI.md
via type-dispatched formatter so new resource types just add a case
CLI
- multica project create --repo <url> attaches repos in one step
- multica project resource add/list/remove
Frontend
- Project create modal: Repos pill (workspace repos + ad-hoc URL)
- Project detail sidebar: collapsible Resources section with attach/remove
Docs
- New "Project Resources" chapter explaining the abstraction and
exactly what code to touch when adding a new resource type
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): transactional resources[] on create + generic CLI ref + test fix
Addresses review feedback on PR #1926:
1. CI red: TestProjectResourceLifecycle delete step called withURLParam
twice, which replaced the chi route context and dropped the project id.
Switched to the existing withURLParams helper from daemon_test.go.
2. POST /api/projects now accepts resources[] and attaches them in the
same transaction as the project. Invalid refs roll back the whole
create — no more half-attached projects on failure. Web modal + CLI
`project create --repo` both use the new bundled payload.
3. CLI `project resource add` now accepts a generic --ref '<json>' flag
so a new resource_type works without a CLI change. Per-type
shortcuts (--url for github_repo) remain as a convenience but are no
longer the only way in. Docs updated to drop the CLI from the
"files you must touch" list.
Adds two new server handler tests:
- TestCreateProjectAttachesResources (resources[] happy path)
- TestCreateProjectRollsBackOnInvalidResource (transactional rollback)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
8c9c52b023 |
feat(inbox): add notification preferences to control inbox noise by event type (#1906)
Users can now mute specific notification categories (assignments, status changes, comments & mentions, priority/due-date updates, agent activity) from Settings > Notifications. Muted event types are silently filtered at notification creation time — no inbox items are created for muted groups. - Add notification_preference table (migration 064) - Add GET/PUT /api/notification-preferences endpoints - Filter notifications in notifyIssueSubscribers, notifyDirect, and notifyMentionedMembers based on user preferences - Add Notifications tab in Settings with per-group toggle switches |
||
|
|
cfa38df97b |
feat(quick-create): gate on daemon CLI version with pre-check + server enforcement (#1857)
* fix(quick-create): bound dialog height + scroll editor when content overflows Pasting a screenshot into the agent-create prompt expanded the editor unbounded, which dragged DialogContent past the viewport since the agent mode className had no max-height. Manual mode was unaffected because manualDialogContentClass pins `!h-96`. - Cap agent-mode DialogContent at `!max-h-[80vh]` (width stays `!max-w-xl`); short prompts still render compact, tall content stops at 80% of the viewport. - Switch the editor wrapper to `flex-1 min-h-[140px] overflow-y-auto` so it absorbs the remaining vertical space inside the now-bounded DialogContent and scrolls internally instead of pushing the dialog. * feat(quick-create): gate on daemon CLI version with pre-check + server enforcement The agent-create flow depends on multica CLI behavior introduced in v0.2.20 (URL attachment handling, no-retry semantics on `multica issue create` failure — see PR #1851 / MUL-1496). Older daemons either double-create issues on partial CLI failures or mishandle pasted screenshot URLs. Per J's review on MUL-1496, gate the flow at two layers — frontend pre-check for fast feedback, server re-check as the trust boundary, both fail-closed on missing/unparsable versions. Server: - New MinQuickCreateCLIVersion + CheckMinCLIVersion helper in pkg/agent (with sentinel errors for missing vs too-old). - QuickCreateIssue handler reads runtime metadata.cli_version and returns a stable 422 { code: "daemon_version_unsupported", current_version, min_version, runtime_id } before enqueuing. - The check runs after the existing online + ownership validation, so all rejections surface uniformly through the modal's existing error path. Frontend: - New @multica/core/runtimes/cli-version with the min version constant, parser, and runtime-metadata reader (tiny semver, no new lib dep). - AgentCreatePanel resolves the selected agent's runtime, runs the same check, shows an inline amber notice below the agent picker when missing/too old, and disables the Create button. - Submit handler also catches the server's 422 (defensive race — runtime can re-register between pre-check and submit) and surfaces the same wording in the error row. Switching to manual create remains a clean escape hatch — manual mode doesn't talk to a daemon at all, so an outdated CLI doesn't block the user from filing the issue. |
||
|
|
4ad0a0b847 |
feat(chat): presence v4 — status pill, failure bubble, elapsed timing (#1856)
A complete UX upgrade for chat sending → receiving → recovering.
* StatusPill replaces the orphan spinner — stage-aware copy
("Reading files · 12s", "Searching the web · 14s", "Typing · 24s"),
shimmer text, monotonic timer, derived effective status, > 60s
warning tone, > 5min cancel button.
* WS writethrough on task:queued / task:dispatch / task:cancelled so
pendingTask cache stays in sync with the daemon state machine without
invalidate-refetch latency. broadcastTaskDispatch now includes
chat_session_id when the task is for a chat session — the existing
payload only carried it on the generic task: events, leaving the pill
stuck at "Queued" until completion.
* Failure fallback — FailTask writes a chat_message tagged with
failure_reason (mirrors the issue path's system comment, gated on
retried==nil). Front-end renders an inline note ("Connection failed",
with a Show details collapsible) instead of the previous black hole.
* Elapsed timing — chat_message.elapsed_ms persists task.completed_at -
task.created_at on success/failure rows. UI shows "Replied in 38s" /
"Failed after 12s" beneath assistant bubbles. Format helper shared
between StatusPill and the persisted caption so the live timer and
final reading never disagree.
* Optimistic burst rebalanced — pendingTask seed + created_at moved
before the HTTP roundtrip so the pill appears the instant the user
hits send; handleStop is fire-and-forget so cancel feels immediate
(server confirmation arrives via task:cancelled WS).
* Presence integration — chat avatars use ActorAvatar (status dot +
hover card); OfflineBanner above the input on offline/unstable;
SessionDropdown shows per-row in-flight/unread pip plus a
cross-session aggregate pip on the closed trigger.
* Editor blur on send so the caret stops competing with the StatusPill
/ streaming reply for the user's attention.
* Chat panel isOpen now persists globally; defaults to OPEN for new
users (storage key absence) so the feature is discoverable. Existing
users' prior choice is respected.
* DB: migrations 062 (failure_reason) + 063 (elapsed_ms), both
ADD COLUMN NULL — fast, non-blocking, backwards compatible.
* WS: task:failed chat path now invalidates chatKeys.messages — fixes
a pre-existing bug where the failure bubble required a page refresh
to appear.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
286ecf04b1 |
feat(daemon): add WebSocket heartbeat with HTTP fallback
Adds daemon WebSocket heartbeat acknowledgements while preserving HTTP heartbeat fallback and HTTP task claim/result paths. Keeps old daemon compatibility and task wakeup behavior intact. |
||
|
|
86e7de3e41 |
feat(server/auth): cache auth token lookups in Redis with 10m TTL
* feat(server/auth): cache PAT lookups in Redis with 60s TTL
Personal access tokens used to hit Postgres on every request: a SELECT
to resolve token_hash → user_id, plus a fire-and-forget UPDATE of
last_used_at. For a CLI / daemon making many requests per second this
is wasted DB load — the token is the same and the answer hasn't changed.
Add a Redis-backed cache (auth.PATCache) keyed by token hash, TTL 60s:
- On cache hit, the auth middleware skips both the SELECT and the
last_used_at UPDATE. last_used_at is now refreshed at most once per
TTL window per token, not per request.
- On cache miss the middleware falls back to today's behavior: query
Postgres, populate the cache, async-update last_used_at.
- On revoke, the handler invalidates the cache entry so revocation
takes effect immediately rather than waiting for the TTL to expire.
This required changing RevokePersonalAccessToken from :exec to :one
RETURNING token_hash.
The cache is nil-safe: when REDIS_URL isn't configured, NewPATCache
returns nil and the middleware degrades to today's always-hit-DB
behavior. JWT validation is untouched (already DB-free).
Tested with REDIS_TEST_URL — same gating pattern the rest of the
suite uses for Redis-backed tests. New tests cover nil-safety, set/
get/invalidate, TTL, and the middleware short-circuit on cache hit.
* fix(server/auth): clamp PAT cache TTL to token's remaining lifetime
GPT-Boy review caught: a PAT expiring in <60s would still be cached
for the full PATCacheTTL window, so the token could continue passing
auth on cache hit for up to ~60s after its expires_at. The DB query
filters expired tokens (revoked = FALSE AND expires_at > now()), but
that filter never ran on a cache hit.
Make Set take an explicit ttl, and add TTLForExpiry to compute it:
- no expires_at → full PATCacheTTL
- expires_at far → full PATCacheTTL
- expires_at <60s → time until expiry
- already expired → 0, Set skips caching (TOCTOU defense between
the SELECT and the Set, since the SELECT
already filters expired rows)
Regression test pins the clamp behavior end-to-end against Redis.
* feat(server/auth): cache daemon-token + PAT lookups in DaemonAuth, bump TTL to 10m
Daemon /api/daemon/* requests (heartbeat, claim task) hit DaemonAuth
which previously did its own GetDaemonTokenByHash on every request and
*also* duplicated the PAT lookup on the mul_ fallback — bypassing the
cache added in
|
||
|
|
391a4ecd09 |
feat: add backend default agent args env vars (#1807)
* feat: add backend default agent args env vars * docs: document default agent args env vars |
||
|
|
9baa72cc68 |
fix: polish quick-create UX (kind labeling, dark toast, placeholder) (#1831)
* fix: polish quick-create UX (kind labeling, dark toast, placeholder)
Three small fixes shaken out from using the agent-create flow:
- AgentTaskResponse now carries a `kind` discriminator
("comment" | "autopilot" | "chat" | "quick_create" | "direct"), computed
from the existing FK shape with no extra DB access. The Activity row
uses it to label quick-create tasks as "Creating issue" instead of
falling through to the generic "Untracked" — once the agent finishes
and the new issue is linked, the row transitions to the normal
identifier+title display.
- Sonner Toaster reads `resolvedTheme` instead of `theme`, so toasts
follow the actual dark/light state. Forwarding "system" let sonner
pick its own answer from `prefers-color-scheme`, which in the Electron
renderer can disagree with next-themes' `html.dark` class — the toast
rendered light on a dark UI.
- Agent-create placeholder rephrased to a more conversational example
with a project reference: "let Bohan fix the inbox loading slowness
in the Web project". Drops the priority hint (priority isn't widely
used) and matches how people actually instruct the agent.
* fix(quick-create): link new issue back to task on completion
Addresses the review on PR #1831: completed quick-create tasks were
left with issue_id=NULL forever, so the activity row stayed on
"Creating issue" instead of transitioning to the normal MUL-XXX +
title rendering once the agent finished.
- Server: notifyQuickCreateCompleted now writes the resolved issue id
back to agent_task_queue.issue_id via a new LinkTaskToIssue query
(guarded by `issue_id IS NULL` so it only ever fills the unset
quick-create case). Best-effort: a write failure logs but doesn't
block the inbox notification.
- Frontend: defensive wording fallback — kind=quick_create rows in
terminal status (completed/failed/cancelled) now render as
"Quick create" instead of the active "Creating issue" label,
covering rows whose link write failed or whose agent never
produced an issue at all.
|
||
|
|
f745a3bbbe |
feat(agent): presence v3 + execution log + trigger summary (#1823)
* refactor(views): migrate agent/runtime/skill lists to TanStack DataTable
Replace the per-page CSS Grid + minmax(min, fr) + sticky-first-col + truncate
implementation with a TanStack Table backend rendered through a Dice UI-style
DataTable shell. Column widths are now px-based via column.size, so cells
no longer shrink or auto-truncate as the viewport narrows; when the sum of
columns exceeds the viewport, the container scrolls horizontally instead.
- Add @tanstack/react-table to the catalog (8.21.3) and wire it into
packages/ui (dep) and packages/views (peerDep).
- packages/ui: new DataTable + DataTableColumnHeader + lib/data-table.ts
(getColumnPinningStyle), adapted from Dice UI's registry. The shell
renders <table> directly (skipping shadcn's <Table> wrapper) so its own
outer overflow controls both axes — no nested overflow conflicts.
- packages/views: each list now declares ColumnDef[] with explicit
cell renderers. Row click navigates to detail via onRowClick (instead of
wrapping <tr> in <a>, which is invalid HTML); kebab dropdowns
stopPropagation so they don't trigger the row navigation.
- Drop the previous AGENT_LIST_GRID / GRID_WITH_OWNER / ROW_GRID
templates and the sticky-first-col / subgrid mechanics that came with
them. agent-list-item.tsx is removed; runtime-list.tsx and
skills-page.tsx are trimmed to thin wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent): cap description at 255 chars (db + api + ui)
Symmetric enforcement across DB, server, and UI:
- Migration 060: pre-flight truncate of any oversize rows, then ADD
CONSTRAINT NOT VALID + VALIDATE CONSTRAINT so the new check doesn't
block writes during validation.
- Server handler validates utf8.RuneCountInString on Create/Update and
rejects over-limit input with 400.
- Front-end gets AGENT_DESCRIPTION_MAX_LENGTH in core/agents/constants
(single source of truth shared by the create dialog + edit modal +
test suite) and a CharCounter component that warns at 90% and errors
past the cap.
- Description editor moves from a 288px popover to a roomy modal.
Editor body is mounted only while the dialog is open, so the local
draft state is locked in at mount time and never reset by an external
WS update — the React-recommended replacement for the
useEffect(reset, [value]) anti-pattern.
Counted in code points everywhere (rune count / spread length /
char_length) so multibyte input agrees across all three layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(views): data-table polish across runtime + skill lists
Builds on the DataTable migration in
|
||
|
|
2d9c153695 |
feat: quick-create issue (async agent + inbox completion) (#1786)
* feat(server): add quick-create issue async task path Adds POST /api/issues/quick-create which validates the picked agent's reachability up front (not archived, has runtime, runtime online) then queues an issue-less agent task whose context JSONB carries the user's natural-language prompt + requester + workspace. Daemon claim resolves the workspace from the context, and the prompt builder switches to a quick-create template instructing the agent to translate the prompt into a single multica issue create call. Task completion writes a success inbox item to the requester pointing at the newly-created issue (located by querying the agent's most recent issue in the workspace since task start, so we don't depend on agent stdout shape). Failures write an action_required inbox item carrying the original prompt + agent id so the frontend can offer "Edit as advanced form" without losing input. * feat(views): quick-create issue modal + inbox failure CTA Adds a streamlined create-issue UI bound to the c shortcut: pick an agent, type one line, submit. The modal closes immediately and the agent translates the prompt into a multica issue create call in the background. Shift+c keeps the legacy advanced form for users who want every field. The "Advanced" button inside the new modal seeds the shared issue-draft store with the prompt + picked agent so switching mid-flow doesn't lose input. Last-used agent persists per (user, workspace) via a workspace-aware zustand store so frequent users skip the picker on every open. Inbox renders quick_create_done items with a status pin to the new issue and quick_create_failed items with an "Edit as advanced form" CTA that re-seeds the legacy modal with the original prompt. ApiError now carries the parsed JSON body so the modal can branch on the structured agent_unavailable code without parsing the error message. * fix(quick-create): execenv injection, claim race, private-agent permission Addresses GPT-Boy review on #1786: 1. execenv was rendering the assignment-task issue_context.md / runtime workflow even for quick-create, telling the agent to call `multica issue get/status/comment add` against an empty IssueID. Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create branch in renderIssueContext + the runtime_config workflow that instructs the agent to run a single `multica issue create` and exit, with explicit "do NOT call issue get/status/comment add" guards. 2. ClaimAgentTask serialized only on issue_id / chat_session_id, so concurrent quick-creates on the same agent (both NULL on those columns) ran in parallel — making the success-inbox lookup race over "most recent issue by this agent". Adds a third OR clause that treats "all four FKs NULL" as a serialization key for the same agent, so quick-create tasks on a given agent run one at a time. 3. QuickCreateIssue handler bypassed the private-agent ownership rule that validateAssigneePair enforces elsewhere — a user could POST a private agent_id they didn't own and trigger it. Now routes the picked agent through validateAssigneePair before the runtime liveness check. 4. Clarifies the quick-create-store namespacing comment to match the actual workspace-aware StateStorage convention used by the other issue stores (per-user is browser-profile-local). * fix(quick-create): branch Output section + deterministic origin lookup Addresses GPT-Boy's second-pass review on #1786: 1. The runtime_config.go Output section forced "Final results MUST be delivered via multica issue comment add" for every non-autopilot task — quick-create still got this conflicting instruction even though there's no issue to comment on. Switched the Output block to a three-way switch so quick-create gets a tailored "stdout is captured automatically; do NOT call comment add" branch matching the autopilot variant. 2. Completion lookup was "most recent issue created by this agent since task.started_at", which races against concurrent issue creates by the same agent (assignment task running alongside quick-create when max_concurrent_tasks > 1). Replaced with a deterministic origin link: - Migration 060 extends issue.origin_type CHECK to allow 'quick_create'. - Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a quick-create task. - multica issue create CLI reads the env var and stamps the new issue with origin_type=quick_create + origin_id=<task_id>. - Server CreateIssue handler accepts (origin_type, origin_id) from trusted callers (only "quick_create" is allowed; the pair is rejected unless both fields are provided together). - notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on (workspace_id, "quick_create", task.ID) — no more time-window racing against parallel agent activity. The old GetRecentIssueByCreatorSince query is removed. |
||
|
|
805071b5b1 |
fix(agent/cursor): route Windows launcher through PowerShell -File to preserve multi-line prompts (#1709)
On Windows the official cursor-agent installer ships cursor-agent.cmd whose body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd file goes through cmd.exe, and `%*` in a batch file is expanded by re-tokenising the original command line, which mangles arguments containing newlines or other whitespace - most notably a long, multi-line `-p <prompt>`. The agent then only sees a truncated prompt and fails with "Workspace Trust Required" or exits 1 immediately. When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so Go's os/exec passes each argv as a discrete token. This is exactly what the .cmd does internally; we just skip the cmd.exe re-tokenisation step. PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then powershell.exe on PATH, and finally falls back to %SystemRoot%\System32\WindowsPowerShell\v1.0. Platform-specific code is split via build tags (cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows builds carry no Windows-only dependencies. The lookup is exposed as a package variable to make the Windows path fully unit-testable without spawning real PowerShell. Five unit tests cover: passthrough on non-launcher targets, successful rewrite with a multi-line prompt, .exe direct launch (skip), missing .ps1 (skip), and missing PowerShell host (skip). The change leaves macOS / Linux behaviour entirely untouched and stays on the official cursor-agent launch chain - no node.exe direct invocation, no prompt mutation, no extra flags. Closes #1297 Made-with: Cursor |
||
|
|
21e3cfaa01 |
Agent runtime status redesign: split presence into availability + last-task (#1794)
* feat(agent-status): add workspace live-tasks endpoint and TaskFailureReason type Lays the API + type contract for the front-end agent presence cache: - New `GET /api/active-tasks` returns active (queued/dispatched/running) tasks plus failed tasks within the last 2 minutes for the current workspace. The 2-minute window powers a UI-side auto-clearing "Failed" agent state without back-end pollers. - `agent_task_queue` has no workspace_id column, so the query JOINs agent; `SELECT atq.*` keeps `failure_reason` (migration 055) on the wire. - Adds `TaskFailureReason` to `AgentTask` so the UI can map the 5 backend classifiers (agent_error / timeout / runtime_offline / runtime_recovery / manual) to copy without parsing free-text errors. - New `api.getActiveTasksForWorkspace()` client method; workspace is resolved server-side from the X-Workspace-Slug header (no path param, matching /api/agents and /api/runtimes conventions). Includes the joint engineering plan and designer brief that scope the broader Agent / Runtime status redesign — Phase 0 is this contract plus the front-end derivation layer landing in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent-status): derive presence/health states with WS sync and desktop IPC bridge Adds the front-end derivation layer that turns raw server data into the user-facing 5-state agent / 4-state runtime enums. UI files are deliberately untouched in this commit — derivation lives behind hooks (useAgentPresence, useRuntimeHealth) that any component can call with zero additional network traffic. Architecture: - Derivation is pure functions in packages/core/{agents,runtimes}; the back-end stays free of UI translation. Agents algorithm: runtime offline > recent failed (2-min window) > running > queued > available. Runtimes algorithm: status + last_seen_at -> online / recently_lost / offline / about_to_gc. - A single workspace-wide active-tasks query backs all per-agent presence reads, eliminating N+1 across hover cards, list rows, and pickers. 30-second tick re-renders the hooks so the failed window expires even when no underlying data changes. - WS task lifecycle events (dispatch / completed / failed / cancelled) invalidate active-tasks via the prefix dispatcher. completed/failed were removed from specificEvents so they go through both the prefix invalidate and the existing chat ws.on() handlers. Reconnect refetch picks up active-tasks too. - Desktop bridges window.daemonAPI.onStatusChange directly into the runtimes cache via setQueryData, giving the local daemon sub-second feedback (vs. 75s server sweep). Bridge is wsId-bound so workspace switches automatically rebind the subscription; daemon_id matching covers the same-daemon-multiple-providers case. 24 derivation unit tests cover all branches plus null/empty/boundary inputs (FAILED_WINDOW_MS edges, null last_seen_at, missing completed_at). Full core suite: 112 tests passing. Typecheck green across all 8 workspace packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent-status): redesign agent runtime status as two orthogonal dimensions Splits the conflated 5-state agent presence into two independent axes: - AgentAvailability (3-state): online / unstable / offline — drives the dot indicator everywhere a dot appears. Pure runtime reachability; never sticky-red because of a past task outcome. - LastTaskState (5-state): running / completed / failed / cancelled / idle — surfaced as text + icon on focused surfaces (hover card, agent detail page, agents list, runtime detail). Never colours the dot. Major changes: * Domain layer: AgentPresence union → AgentAvailability + LastTaskState. derive-presence split into deriveAgentAvailability + deriveLastTaskState + deriveAgentPresenceDetail orchestrator. Tests reorganised into three groups (availability invariants, last-task invariants, composition). * Visual config: presenceConfig (5 entries) → availabilityConfig (3) + taskStateConfig (5). availabilityOrder + lastTaskOrder for filter chips. * Workspace-level presence prefetch: new useWorkspacePresencePrefetch hook + WorkspacePresencePrefetch mount component, wired into DashboardLayout (web) and WorkspaceRouteLayout (desktop). Hover cards render synchronously with no skeleton flash on first hover. * ActorAvatar hover: flipped default — disableHoverCard removed, enableHoverCard added (default false). Opt-in at ~14 decision-moment surfaces; pickers / decoration sub-chips stay plain. Status dot decoupled (showStatusDot prop) so picker rows can show presence without nesting popovers. * Hover cards: AgentProfileCard simplified — availability dot only, Detail link top-right (logs live on the detail page). New MemberProfileCard mirrors the structure: name + role + email + top-2 owned agents (sorted by 30d run count) with click-through to agent detail. * Agents list: split Status into two columns — availability (3-color dot + label) and Last run (task icon + label, optional running counts). Two independent filter chip groups (Status + Last run); combination acts as intersection ("online + failed" finds broken- but-alive agents). * Other UI surfaces (issue list/board/detail, comments, autopilots, projects, runtimes, mention autocomplete, subscribers picker) updated to the new dot semantics; status dot now strictly 3-color. Server changes accompany the client redesign — workspace-wide agent-task-snapshot endpoint, runtime usage queries, etc. — to feed the derive layer with the data it needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent-detail): drop last-task chip from detail header + inspector The Recent work section on the agent detail page already shows the same data (with task titles, timestamps, error context) — surfacing "Completed" / "Failed" / etc. up in the header was redundant chrome. Detail surfaces now show only the 3-state availability dot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tables): handle narrow viewports across agents / skills / runtimes Three table layouts were squeezing content into adjacent cells at intermediate widths. Each fix is small and targeted: * runtime-list: the Runtime cell's base name had `shrink-0`, so it refused to truncate when its grid column was narrowed under width pressure — the name visually overflowed into the Health column ("ClaudeOnline" etc). Removed shrink-0, added truncate. The Health column was also a fixed 9.5rem reservation for the worst-case "Recently lost · 2m 14s ago" copy; switched to minmax(0,1fr) so it competes fairly with Runtime. * skills-page: had a single grid template with no responsive breakpoints — all 6 columns were rendered at any width and got visually jammed below md. Added a <md template that drops Source + Updated; the row markup hides those cells via `hidden md:block` / `md:contents`. * agent-list-item: the new Last run column was reserved at minmax(8rem, max-content); on narrow md viewports the 8rem floor pushed the row past available width. Changed to minmax(0,max-content) so the cell shrinks under pressure (its content already truncates). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent-card): hover-only Detail + add Runtime row + breathing room Three small polish tweaks to the agent hover card: - Detail link gets `mr-1` + fades in only on card hover (group-hover). It was visually flush against the popover edge and competing for attention; now it stays out of the way during a quick glance and surfaces only when the user is dwelling on the card. - Runtime row is back, in the meta block (cloud/local icon + runtime name). The earlier removal was over-aggressive — knowing where an agent runs is part of "who is this agent". The wifi badge stays dropped because the availability dot in the header already conveys reachability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(runtime): wifi-style health icon (4-state) for runtime list + agent card Replaces the 6px coloured dot with a wifi-shape icon that carries both state (Wifi vs WifiOff) and severity (success/warning/muted/destructive). Mapping: - online → Wifi (success) - recently_lost → WifiHigh (warning) — transient hiccup, fewer bars - offline → WifiOff (muted) — long unreachable - about_to_gc → WifiOff (destructive) — sweeper coming soon Used in two places: - Runtime list: replaces HealthDot in the dedicated leading-icon column. Bumped the column from 0.5rem (dot-sized) to 0.875rem (icon-sized). - Agent profile card RuntimeRow: derives runtime health from runtime + clock (matching the 4-state semantics) and renders HealthIcon next to the runtime name. Cloud runtimes always read as online. The duplicate signal with the header availability dot is intentional — it confirms WHICH runtime is the one currently in the dot's state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
03f3180b8f |
fix(agent): ignore Kiro session/load history replay (#1789)
Ignore Kiro ACP session/load history replay before the active prompt starts; keep task messages, usage, and tool state scoped to the current Kiro turn. Verified with go test ./pkg/agent -run TestKiro, go test ./pkg/agent, and git diff --check origin/main...HEAD. |
||
|
|
c366cf2ba1 |
feat(agent): add Kiro CLI ACP runtime (#1780)
* feat(agent): add kiro cli acp runtime * fix(agent): align kiro acp prompt and notifications * chore(agent): clarify kiro acp args compatibility |
||
|
|
abd69890a8 |
Revert "feat(issues): server-side filters incl. label, fixing pagination drop…" (#1779)
This reverts commit
|
||
|
|
246fcd4ce4 |
feat(issues): server-side filters incl. label, fixing pagination drops (#1776)
* feat(issues): server-side label + filter querying for issue list Extends GET /api/issues with label_ids, priorities, creator_ids, project_ids, include_no_assignee, and include_no_project params, and moves the existing single-value filters onto array-form. Each filter becomes part of the SQL WHERE clause so paginated buckets reflect the user's selection — fixes the bug where client-side filtering hid matches sitting past the first page (#1491). CLI gains a repeatable --label flag; legacy --priority/--assignee/ --project keep working via the single-value compatibility paths. * feat(issues): drive workspace + my-issues filters from the server issueListOptions and myIssueListOptions now key the React Query cache on a normalized filter object, so each filter combination has its own cache entry and a filter change re-fetches with the wire-shape filter applied server-side. Drops the client-side filterIssues step on the issues page, my-issues page, and project detail — that step silently hid matches that lived past the first paginated page (#1491). Adds a Label submenu to the workspace issues filter dropdown, plus labelFilters in the view store. Mutations and ws-updaters fan their optimistic patches across every filter-keyed list cache via qc.setQueriesData on issueKeys.listPrefix(wsId), and the editor's mention-suggestion reads from any matching list cache for instant first paint regardless of which filter is active. * fix(issues): route Members/Agents scope through server-side filter The Members/Agents scope tabs on the workspace issues page were still narrowing client-side via `assignee_type === 'member'`. That hits the exact pagination-blind bug this PR is meant to fix: if the first 50 issues per status don't include the right assignee type, the tab shows "No issues" while later pages have matches. Adds an `assignee_types text[]` filter to ListIssues / ListOpenIssues / CountIssues, threads it through the API client, normalizer and view filter, and maps the scope tab to it. Each scope now keys its own list cache and refetches with the correct first page. Also disables the My Issues "My Agents" query when the user owns no agents — `assignee_ids: []` was getting dropped by both the API client and the query-key normalizer, so the request went out unfiltered and surfaced unrelated issues under "My Agents". |
||
|
|
9db91e89f5 |
feat: add daemon websocket task wakeups (#1772)
* feat: add daemon websocket task wakeups * feat: fan out daemon wakeups across nodes * fix: dedupe daemon wakeup loopback events * fix: lengthen daemon polling fallback interval --------- Co-authored-by: Eve <eve@multica.ai> |
||
|
|
b77acdf642 |
fix(comments): cancel triggered tasks when comment is deleted (#1747)
When a user deletes a comment that triggered an agent task, the agent would still run with the now-deleted content baked into its prompt (fetched at task claim time) — manifesting as "the agent still sees the deleted comment". The FK ON DELETE SET NULL only nullified trigger_comment_id; the queued task itself was never cancelled. DeleteComment now cancels any queued/dispatched/running task whose trigger is the deleted comment, before the comment row is removed. |
||
|
|
6bd5bbad9c |
fix: timeout stalled Codex turns (#1730)
* fix: timeout stalled codex turns * fix: count codex progress events as activity |
||
|
|
6620997503 |
feat(issues): render labels on list/board with bulk server-side fetch (#1741)
* feat(issues): render labels on list/board with bulk server-side fetch ListIssues / ListOpenIssues / GetIssue now bulk-fetch labels per response via a new ListLabelsForIssues query so the client gets labels in a single round-trip instead of N requests per visible issue. List-row and board-card read issue.labels directly; an issue_labels:changed WS handler patches the list and detail caches in place so chips stay live across tabs, and attach/detach mutations mirror their result into the same caches for immediate same-tab feedback. Adds a "Labels" toggle to the card properties dropdown (defaults on). * fix(issues): preserve cached labels and refresh on label edit/delete Three fixes from gpt-boy's review of #1741: 1. IssueResponse.Labels was a non-omitempty slice, so paths that didn't load labels (UpdateIssue, batch updates, the issue:updated WS broadcast) serialized labels:null. onIssueUpdated then merged that null into the list/detail caches, wiping chips on every other tab whenever any non- label field changed. Switched to *[]LabelResponse + omitempty: nil = field absent (client merge keeps existing labels); non-nil (incl. empty slice) = authoritative. 2. issue.labels is a denormalized snapshot, but useUpdateLabel / useDeleteLabel and the WS label:* prefix only touched labelKeys, leaving stale chips in list/board after rename/recolor/delete. Mutations now also invalidate issueKeys.all(wsId), and the realtime refreshMap maps the label prefix to both labels and issues invalidation for cross-tab. 3. Persisted cardProperties from before this branch lacks the new `labels` key. Render fell back to `?? true` but the dropdown switch read it raw and showed unchecked. Added a custom Zustand merge that deep-merges cardProperties so newly added toggles inherit defaults for existing users; dropped the `?? true` fallbacks now that the store guarantees the key. |
||
|
|
e9d04ecfc1 |
feat(labels): ship issue labels (closes #1191) (#1233)
* feat(labels): add issue label CRUD + attach/detach handlers (#1191) The issue_label and issue_to_label tables were scaffolded in 001_init.up.sql but never wired to any code path. This commit ships the backend for #1191: - Migration 048: adds created_at/updated_at timestamps + workspace-scoped case-insensitive unique index on label names - sqlc queries for label CRUD + issue<->label attach/detach + batch list (ListLabelsByIssueIDs for board/list views) - HTTP handlers: /api/labels CRUD, /api/issues/{id}/labels attach/detach - Protocol events: label:{created,updated,deleted} + issue_labels:changed - Handler tests covering CRUD, duplicate-name conflict, invalid-color, attach/detach idempotency, and cross-workspace isolation * feat(cli): add label and issue label subcommands (#1191) - multica label {list,get,create,update,delete} - multica issue label {list,add,remove} Both follow existing CLI conventions (JSON/table output, flag shapes) and exercise the /api/labels endpoints shipped in the previous commit. * feat(web): add labels UI — picker with inline create + management dialog (#1191) Exposes the backend label feature to users via the existing issue-detail sidebar. - `@multica/core/types/label` — Label, CreateLabelRequest, UpdateLabelRequest, plus response envelopes - `@multica/core/api/client` — 8 methods for label CRUD and issue↔label attach/detach - `@multica/core/labels` — labelKeys, queryOptions, and mutation hooks with optimistic updates (matches the project/ module layout) - WS event type literals extended for label:{created,updated,deleted} and issue_labels:changed - `views/labels/label-chip.tsx` — colored pill; uses relative luminance (ITU-R BT.601) to pick #111827 or #f9fafb text so chips stay readable on both pastel and saturated backgrounds - `views/issues/components/pickers/label-picker.tsx` - Multi-select combobox in the issue sidebar - When 0 labels: "Add label" trigger - When 1+ labels: the chips themselves are the trigger; × on each chip detaches without opening the picker - Inline create: typing a new name + Enter creates with a hash-derived color and attaches in one motion (matches Linear/GitHub) - "Manage labels…" footer opens a dialog containing the full workspace panel — users never leave the issue context to rename/recolor/delete - `views/issues/components/labels-panel.tsx` — workspace labels manager. Single-row create form (color swatch + name + Add button). Each label row supports inline rename + recolor + delete (with confirm dialog). Color input uses the browser's native picker for full-gamut access — no preset palette clutter. - `PropRow label="Labels"` added to the issue-detail sidebar below Project Labels are issue metadata everyone uses — not admin configuration. Putting them in Settings next to destructive workspace actions misframed them; adding a top-level nav entry or a sibling tab to the Issues page added surface area that wasn't earning its keep for a feature users touch occasionally. Keeping management in a dialog launched from the picker itself keeps users in their issue context and matches how GitHub handles label editing from the label selector. |
||
|
|
ba2f19d631 |
fix: refresh agent status from active tasks (#1733)
Co-authored-by: Eve <eve@multica.ai> |
||
|
|
8b340fcf21 |
fix(agent/opencode): bypass npm .cmd shim on Windows to preserve multi-line prompts (#1718)
* fix(agent/opencode): bypass npm .cmd shim on Windows to preserve multi-line prompts The npm-generated `opencode.cmd` shim forwards argv via Windows batch `%*`, which silently truncates positional arguments at the first newline. The daemon spawns OpenCode with a multi-line prompt (system prompt + user message), so on Windows the agent only ever sees the first line and responds generically as if it never received the user's message (reported in #1717 with native-binary repro confirming the same prompt arrives intact when cmd.exe is skipped). When `runtime.GOOS == "windows"` and `exec.LookPath` returns a `.cmd` shim, walk to the native binary that npm bundles next to the shim: <prefix>\opencode.cmd <prefix>\node_modules\opencode-ai\node_modules\opencode-windows-x64\bin\opencode.exe If the native binary is missing (unusual install layout), keep the original shim path so PATH lookup still wins. The resolver is a pure function with an injectable `statFn`, so layout assertions are testable on Linux: - shim resolves to the bundled native binary - missing native returns "" (caller keeps original path) - non-cmd paths (Linux/Mac binary, opencode.exe direct, empty) skip resolution - uppercase `.CMD` is accepted (PATHEXT entries can be either case) Closes the user-facing failure mode without restructuring exec resolution across the rest of the agent backends — the other shim-aware fixes can follow the same shape if/when they land in similar repros. * fix(agent/opencode): cover x64-baseline and arm64 npm package variants `npm install -g opencode-ai` ships three Windows platform packages (opencode-windows-x64, opencode-windows-x64-baseline for older CPUs without AVX2, opencode-windows-arm64 for Surface / Copilot+ PC) and installs whichever matches the host. The previous resolver only knew about opencode-windows-x64, so baseline-x64 and arm64 hosts would fall back to the .cmd shim and hit the multi-line prompt truncation again. Iterate the three package candidates in GOARCH-preferred order. ARM64 hosts try arm64 first; everything else tries x64, then baseline, then arm64 as a last resort. Cost is one extra statFn call per miss when the GOARCH-preferred package isn't installed. Surfaced by review on #1718. * test(agent): add Windows counterpart to writeTestExecutable writeTestExecutable in exec_fixture_unix_test.go is referenced by claude_test.go / codex_test.go / kimi_test.go, but the //go:build unix constraint meant `go test ./pkg/agent` failed to build on Windows. ETXTBSY is a Linux/Unix fork-exec race; Windows doesn't have that pathology, so a plain os.WriteFile is sufficient. Lifted from #1719 (Codex) with attribution. Surfaced by review on #1718. |
||
|
|
aca74293dd |
fix(agent/claude): surface stderr tail on writeClaudeInput failure + lock with e2e test (#1698)
#1674 wired claude's post-handshake error path through withAgentStderr but left the writeClaudeInput failure branch returning a bare "broken pipe" error. That branch fires precisely when claude crashes during startup — exactly when the stderr tail is most useful for root-causing V8 aborts, Bun panics, or missing native modules. cmd.Wait() before sampling Tail() flushes os/exec's internal stderr copy goroutine, matching the Wait→Tail synchronization contract spelled out in stderr_tail.go. Adds TestClaudeExecuteSurfacesStderrWhenChildExitsEarly mirroring the codex test: a fake claude binary drains stdin, writes a V8-abort line to stderr, and exits 3. Locks in the contract that Result.Error carries the stderr tail in the post-handshake failure path on the claude backend too. |
||
|
|
3c3e3bd330 |
fix(task): reconcile agent status when cancelling tasks by issue (#1587) (#1648)
CancelTasksForIssue silently dropped the list of affected tasks, so whenever an issue transitioned to "cancelled" or "done" while a task was still active (6 call sites in issue.go), the underlying agent was left stuck at status="working" indefinitely and required a manual `multica agent update <id> --status idle` to self-correct. This matches the symptom reported in #1587: task rows move to "cancelled" via a non-user-initiated path, agent status never reconciles. Change CancelAgentTasksByIssue from :exec to :many (also tack on completed_at = now() for consistency with CancelAgentTasksByIssueAndAgent), then update CancelTasksForIssue to iterate the returned rows and call ReconcileAgentStatus + broadcast task:cancelled per affected task — mirroring the pattern already used by CancelTask and RerunIssue. No test added; the change is small and mirrors well-covered paths. Happy to add a mock-backed test in a follow-up if reviewers prefer. Refs #1587 Refs #1149 |
||
|
|
6f04a6d26b |
feat(agent): surface agent CLI stderr tail in failure messages (#1674)
Hoist the existing stderrTail ring-buffer (previously codex-only) into a shared pkg/agent helper so every Backend that supervises a child CLI can include the last ~2 KB of that CLI's stderr in Result.Error. Wire the claude backend through the same path. Motivation: claude on Windows occasionally exits with a non-zero status after ~5–8 minutes of a single long-running tool_use, and right now the daemon only reports "claude exited with error: exit status 3" / "exit status 0x80000003" — useless for root-causing V8 aborts, Bun panics, native-module OOMs, or any other CLI-side crash. With the tail attached, the failure message carries the real signal (panic line, V8 assertion, stderr-printed HTTP error) all the way into the task row's error field that users see in the API. Renames withCodexStderr to withAgentStderr(msg, label, tail) so the helper is self-documenting across providers. |
||
|
|
95912243bb |
test(daemon): cover cancelled classification in executeAndDrain (#1692)
Follow-up to #1686. Locks in two nits flagged during review: 1. agent.Result.Status doc comment now lists "cancelled" alongside the existing values, so the enum surface matches actual usage. 2. New TestExecuteAndDrain_ContextCancelled_ReportsCancelled exercises the path added in #1686: when the parent context is cancelled before the backend produces a Result, executeAndDrain must return Status="cancelled" (not "timeout"). A regression here would silently restore the misleading log line we just fixed. |
||
|
|
74593fdb88 |
fix(daemon): use CREATE_NEW_CONSOLE to stop grandchild console popups on Windows (#1521) (#1643)
* fix(daemon): use CREATE_NEW_CONSOLE to stop grandchild console popups on Windows (#1521) CREATE_NO_WINDOW strips the console entirely. When the agent CLI then spawns a console-subsystem grandchild (bash, cmd, netstat, findstr, timeout) without itself passing CREATE_NO_WINDOW, Windows allocates a brand-new visible console window per invocation — trading one popup per agent run for N popups per tool call. Switch to CREATE_NEW_CONSOLE + HideWindow=true so the agent gets a hidden console that grandchildren inherit. Stdio pipes still work via STARTF_USESTDHANDLES; no changes needed at the 17 hideAgentWindow call sites. Add a Windows-only regression test asserting CREATE_NEW_CONSOLE is set and CREATE_NO_WINDOW is not, per the #1474 Windows-test follow-up. Root-cause diagnosis by @matrenitski (verified against the shipped multica.exe and the Claude Code CLI it spawns) in issue #1521. * test(agent): use CREATE_NEW_CONSOLE-compatible flag in preservation test CREATE_NEW_PROCESS_GROUP is silently ignored by Windows when combined with CREATE_NEW_CONSOLE, so asserting it 'survives' was only bitwise-true, not semantically meaningful. Switch the example to CREATE_UNICODE_ENVIRONMENT (documented compatible) and also assert a non-flag field (NoInheritHandles) survives to exercise full struct preservation. |
||
|
|
40cea8454d |
feat(autopilot): redesign modal — simpler schema, consistent schedule UI (#1595)
Drop priority and project_id from autopilot. project_id was never exposed in the UI and priority duplicated the agent's own task queue priority. Redesign the create/edit modal as a Runbook (left) + Configuration (right) layout. Rework the Schedule section around a single visual shell so every picker aligns pixel-for-pixel on the same row: - TimeInput (new): segmented HH:MM control adapted from openstatusHQ/time-picker, driven by keyboard (ArrowUp/Down to step, ArrowLeft/Right to jump segment, digit typing with a 2s two-digit window). Replaces <input type="time">, whose native UI broke the design system. Supports a minuteOnly variant for hourly schedules. - TimezonePicker (new): searchable Popover with a fixed-width left check slot so rows stay aligned and GMT offsets never collide with the selected indicator. - Runbook editor now lives in a bordered card, giving the placeholder an input surface instead of bare document flow. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |