mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 05:49:12 +02:00
agent/lambda/986f8092
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f20999597 |
refactor(timeline): drop server-side comment + timeline pagination (#2322)
* refactor(timeline): drop server-side comment + timeline pagination (MUL-1929) The cursor-paginated /timeline and /comments endpoints were sized for a problem the data shape doesn't have: prod p99 is ~30 comments per issue and the all-time max is ~1.1k. Time-based pagination also splits reply threads across page boundaries (orphan replies), which the frontend was papering over with an "orphan rescue" that promoted disconnected replies to top-level — confusing UX with no real benefit. Replace both endpoints with a single full-issue fetch, capped server-side at 2000 rows as a defensive safety net (never hit in practice). Server - /api/issues/:id/timeline now returns a flat ASC TimelineEntry[] (matches the legacy desktop contract — older Multica.app builds keep working because the wrapped TimelineResponse + cursors are gone, and the raw array shape was always what they consumed). - /api/issues/:id/comments drops limit/offset; only ?since is honoured for the CLI agent-polling flow. - Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest and the timelineCursor encoding. - Replace with ListCommentsForIssue / ListCommentsSinceForIssue / ListActivitiesForIssue (capped by argument). CLI - multica issue comment list drops --limit / --offset and the X-Total-Count reporting; --since is preserved for incremental polling. Frontend - Replace useInfiniteQuery with useQuery in useIssueTimeline; drop fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount. - Remove timeline-cache helpers (mapAllEntries / filterAllEntries / prependToLatestPage) and the TimelinePage / TimelinePageParam types. - WS event handlers update the single flat-array cache directly. - Drop the orphan-reply rescue in issue-detail — every reply's parent is now guaranteed to be in the same array. - Strip the "show older / show newer / jump to latest" buttons and their i18n strings. Co-authored-by: multica-agent <github@multica.ai> * fix(timeline): address review feedback on pagination removal Three issues caught in PR #2322 review: 1. /timeline broke for stale clients between #2128 and this PR. They send ?limit/?before/?after/?around and parse with the wrapped TimelinePageSchema; the new flat-array response was failing schema validation and falling back to an empty timeline. Restore the wrapped shape on those query params (DESC entries, null cursors, has_more_*=false), keeping the flat ASC array for bare requests. Around-mode now also fills target_index from the merged slice so legacy clients can still scroll-to-anchor without a follow-up. 2. The agent prompts in runtime_config.go and prompt.go still told agents that `multica issue comment list` accepts --limit/--offset and to use `--limit 30` on truncated output. With those flags removed in this PR, new agent runs would hit "unknown flag" or skip context. Update the prompt copy to "returns all comments, capped at 2000; --since for incremental polling". 3. useCreateComment's onSuccess was a bare append to the timeline cache with no id-dedupe, so a fast comment:created WS event firing before onSuccess produced a transient duplicate. Restore the id guard the old prependToLatestPage helper used to provide. Adds two new boundary tests: - TestListTimeline_LegacyWrappedShape_OnPaginationParams - TestListTimeline_LegacyWrappedShape_AroundFillsTargetIndex Co-authored-by: multica-agent <github@multica.ai> * test(handler): fix timeline test assertions for handler-package isolation The TestListTimeline_* assertions assumed CreateIssue would seed an "issue_created" activity_log row, but the activity listener that publishes those rows is registered in cmd/server/main.go — handler-package tests don't wire it up. CI saw 5 entries (3 comments + 2 activities) where the test expected ≥6. Drop the auto-activity assumption: assert exactly 5 entries in TestListTimeline_MergesCommentsAndActivities, and tighten TestListTimeline_EmptyIssue to assert a fully-empty timeline. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6400868412 |
fix(timeline): off-by-one — exact-limit comments no longer triggers Show older (#2259)
Pre-fix the gate was `len(comments) >= limit`, which fired even when the issue had EXACTLY <limit> comments. The "Show older" affordance appeared, the user clicked, the next page fetched zero rows. User flagged it on MUL-1857 — "this issue happens to have 30 comments; the button shouldn't appear in that case." The fix is the standard over-fetch probe: ask the SQL for limit+1 rows; if it returned more than limit, drop the extra and report hasMore=true. Otherwise hasMore=false. - New helper `commentOverflow(rows, limit) -> ([]db.Comment, bool)` replaces the count-based `hasMoreCommentsBeyond`. Works for both DESC (latest / before) and ASC (after / around-newer) since both want "keep first <limit>". - All four mode handlers (latest, before, after, around) now ask for limit+1 comments and route through the helper. - Activities still cap at <limit> with no overflow probe — they don't gate pagination (#1857), so the boundary doesn't matter for them. Tests: - TestCommentOverflow pins the truth table with the boundary case ("exactly limit comments" → hasMore=false). - TestListTimeline_ExactlyLimitCommentsHidesShowOlder is the DB-backed regression: 30 comments, limit=30, asserts has_more_before=false and next_cursor=nil. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
161194b86f |
fix(timeline): exclude activities from comment page budget (#2253)
* fix(timeline): exclude activities from comment page budget The /timeline endpoint paginated comments + activities through one shared 50-row budget, so an issue with a chatty agent (status flips, task_completed markers, assignee toggles per run) could trigger "show older" with as few as 10-20 actual comments — users opened the page and thought their discussion had vanished. - Comment limit drops from 50 to 30 (the visible page size users wanted). - has_more_before / has_more_after gate on comments alone via the new hasMoreCommentsBeyond helper. Activity rows still ride along at the same per-call SQL cap but no longer push real comments off-page. - Merge functions stop truncating at the page limit; both pools are individually bounded by SQL, so dropping rows here only re-introduced the bug. The legacy (pre-cursor) path applies its 200-row cap inline. - Test rewrite: TestHasMoreBeyond → TestHasMoreCommentsBeyond, replaced the #2192 merge-truncation regression with a #1857 "dense activity does not hide comments" test that pins the new contract directly. Co-authored-by: multica-agent <github@multica.ai> * fix(timeline): per-pool keyset cursor for comments and activities Pre-fix, next_cursor / prev_cursor anchored on the merged page boundary (oldest / newest entry overall). When activity rows were older than every fetched comment — common on issues created with a status change before the first comment — the latest page emitted a cursor pointing at that activity, and the next "show older" call sent that timestamp into ListCommentsBefore, skipping every unreturned comment in between. GPT-Boy flagged this on PR #2253 with the 80-comment / 30-activity scenario where 50 comments became permanently unreachable. The fix splits the cursor into independent comment and activity positions: - timelineCursor carries (CommentT, CommentID, ActivityT, ActivityID). encode/decode signatures changed accordingly. - New cursorPos type and four bounds helpers (commentBoundsDesc / Asc, activityBoundsDesc / Asc) extract per-pool oldest/newest from fetched rows, with a carry fallback so empty pools advance past the input cursor instead of resetting. - All four mode handlers (latest, before, after, around) now derive cursors from each pool's own bounds. Removed the entryTimestamp / entryID helpers that re-parsed the merged entry slice. Tests: - TestTimelineCursor_RoundTrip pins the encode/decode contract for the new dual-pool format (and rejects garbage input). - TestListTimeline_PerPoolCursorWalksAllComments reproduces GPT-Boy's exact scenario (30 activities older than 80 comments, limit=30) and asserts every comment is reachable through repeated `before=<cursor>` walks. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
099dda0603 |
fix(timeline): include merge-truncation case in has_more_before (#2192) (#2204)
* fix(timeline): include merge-truncation case in has_more_before (#2192) Older comments became unreachable on issues where activity-log entries crowded them out of the latest 50-entry page. The 'show earlier' button was hidden and no cursor was emitted because the has_more_before formula only caught the per-table SQL cap case and missed the in-memory merge truncation case. Reproduces with 48 comments + 49 activities, default limit 50: neither table individually returns >= limit rows, but their sum (97) exceeds the merged page size, so the merge silently drops 47 older comments. The old formula reported has_more_before=false; the client never asked for page 2. Fix: extract hasMoreBeyond(c, a, e, limit) with the missing third disjunct - comments + activities > entries - applied uniformly to listTimelineLatest / Before / After / Around. Backwards compatible: API contract unchanged. Pre-cursor clients (<=v0.2.25) still hit listTimelineLegacy and never read these fields. Newer clients see has_more_before flip from 'wrongly false' to correctly true/false - no field renames, no shape changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(issues): show count badge when activities are coalesced (#2192) The timeline coalesces consecutive same-actor + same-action activities within a 2-minute window so 48 status_changed entries don't take 48 rows. The count badge was only rendered for task_completed / task_failed; for status_changed (and every other action) the coalesced batch silently collapsed to a single line with no hint that N entries were merged. Add a coalesced_badge translation and render '×N' next to the activity text whenever coalesced_count > 1, suppressing it on task_completed / task_failed which already include the count in their translation copy. This pairs with the backend fix for #2192: once the older-comments page becomes reachable again, the activity rows above it should make the density of the merged batch visible rather than misleading the user into thinking only one event happened. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
11a6288cbd |
fix(timeline): legacy array shape for pre-#2128 clients (#2143, #2147) (#2156)
#2128 changed GET /api/issues/:id/timeline from a bare TimelineEntry[] to a wrapped { entries, next_cursor, ... } object. Multica.app ≤ v0.2.25 still in the wild reads the response body as TimelineEntry[] directly, so the moment v0.2.26 backend rolled out, every old desktop hit "timeline.filter is not a function" on any issue open — bug reports landed within ten minutes of the v0.2.26 release (#2143, #2147). The new client always sends ?limit=..., so absence of every pagination param uniquely identifies a legacy caller. Detect that at the top of ListTimeline and serve the old shape (ASC, []TimelineEntry, capped at 200) through a dedicated listTimelineLegacy helper. New clients fall through unchanged. A new TestListTimeline_LegacyShapeForPreCursorClients pins the contract (array shape, ASC order, "[]" not "null" on empty issues). Two existing tests that used the empty query string have been updated to send ?limit=50, since the empty form is now reserved for the compat path. The legacy branch can be deleted once desktop auto-update has rolled the user base past v0.2.26. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
951f51408a |
fix(agent/comments): prevent resumed sessions from reusing stale --parent UUID (#1374)
* fix(agent/comments): re-emit trigger comment id every turn + server-side parent_id guard Resumed Claude sessions keep prior turns' tool calls in context, so a comment-triggered task could reuse the PREVIOUS turn's --parent UUID instead of the current trigger's. The reply landed in the wrong thread (MUL-1125): backend stored exactly what the agent sent, but the agent pulled a stale UUID from its own conversation memory. Two layers of defense: 1. Extract BuildCommentReplyInstructions so daemon.buildCommentPrompt and execenv.InjectRuntimeConfig emit the same "use this exact --parent, do not reuse values from previous turns" block. The per-turn prompt now carries the current TriggerCommentID, which it previously relied on CLAUDE.md for (and CLAUDE.md isn't re-read mid-session). 2. Handler-side guard in CreateComment: when an agent posts from inside a comment-triggered task (X-Agent-ID + X-Task-ID, task has TriggerCommentID), require parent_id == task.TriggerCommentID or return 409. Assignment-triggered tasks are untouched. * fix(agent/comments): scope parent_id guard to the task's own issue Two issues from CI + GPT-Boy's review: 1. Guard was too broad: the CLI stamps X-Task-ID on every request, so an agent legitimately commenting on a different issue while its current task was comment-triggered would get 409'd with the wrong issue's trigger comment id. Narrow the guard to fire only when the request's issue matches the task's own issue — cross-issue agent activity stays unblocked. 2. The integration test tried to insert a second queued task for the same (agent, issue), which hits the idx_one_pending_task_per_issue_agent unique index. Replace the assignment-triggered-task sub-case with a cross-issue regression test (the scenario we now need to cover anyway): post on issue B while X-Task-ID points at a comment-triggered task on issue A, expect 201. |
||
|
|
e7fe6ea79b |
feat(activity): unified activity timeline with comment reply support
Replace the comment-only list with a Linear-style unified timeline that
interleaves field changes and comments chronologically.
Backend:
- activity_listeners.go: records field changes (status, assignee, description,
task completed/failed) to activity_log table on domain events
- Timeline API: GET /api/issues/{id}/timeline merges activity_log + comments
sorted by created_at
- Comment reply: parent_id column + handler support for threading
Frontend:
- Unified timeline replaces comment list: activity entries as compact muted
lines, comments as Card components with reply threading
- Filter toggle (All / Comments / Activity)
- Reply UI: inline editor under comments with Cancel/Reply buttons
- Real-time sync for activity:created + comment events
- 10 new Go tests, all passing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|