mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
* feat(diagnostics): attribute desktop hangs to route, function and stack (MUL-5345) A desktop hang currently reports "froze for 8s" and nothing else, so MUL-5345 could not be diagnosed at all. Three gaps, all fixed here. Route attribution was silently dead. The main window's route reporting lived in the PostHog pageview tracker and was deleted with it (MUL-4127), leaving `getDiagnosticContext` in main reading a WeakMap nothing ever wrote — every field report carried only the asar index.html URL. `DiagnosticRouteReporter` restores the push, and now feeds the in-renderer watchdog too: the renderer runs a memory router, so `location.pathname` could never identify the page either. Paths are bucketed to templates (`/:slug/issues/:id`) before publishing. Function attribution did not exist. The watchdog now prefers `long-animation-frame` over `longtask` where supported, which carries per-script `sourceFunctionName` / `sourceURL` / `sourceCharPosition`. That covers hangs the thread survives. For hangs it does not, main captures the JS call stack over CDP — which requires the Debugger channel to be warmed at window creation, because a command sent after the thread is stuck never gets dispatched. Commands go through a four-verb allowlist, only scalar code locations are copied out of the paused frames (never `scopeChain`, whose handles dereference into user data), and resume is unconditional so a capture can never turn a recoverable hang into a permanent one. Reports could also be lost before delivery. `freeze:get-last` no longer deletes; the renderer sends with `send_instantly` and acks by exact timestamp, so a report killed by a second hang is retried next boot instead of vanishing with the file (the MUL-4115 failure mode: three deterministic hangs, zero events). A 7-day TTL keeps an undeliverable breadcrumb from becoming permanent boot noise. Operations name themselves: `parseMarkdownChunked` marks the diagnostic context before it runs, and the mark travels to main over the async IPC channel that still lands after the main thread stops responding. The event carries how stale the mark is, so it reads as context rather than as a cause. Verified on Electron 39.8.7 / Chromium 142 (throwaway spike, not committed): Debugger.pause during a 12s synchronous block returned the stack in 2ms with the blocking function on top; holding the channel open all session showed no cost beyond run-to-run noise (A/B/A, ordering drift larger than the effect); DevTools and the channel coexist in both open orders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): close the freeze report ack race and stop shipping raw ids (MUL-5345) Two review findings on #5989. Acking on hand-off was not acking on delivery. `onCaptured` fires when posthog.capture() returns; the request is still in flight, and posthog-js exposes no delivery callback to wait on (`CaptureOptions` has `send_instantly` and `transport`, nothing else). Deleting the breadcrumb there loses the report whenever the app freezes again or is killed in that gap — the same MUL-4115 failure the ack protocol was added to prevent. The flush now waits out a grace window before acking: if the process dies inside it the timer never fires, the file survives, and the next boot retries. Duplicates are the accepted trade and are trivially deduped on `breadcrumb_ts`; a lost report is not. Raw identifiers were reaching telemetry. The breadcrumb context was spread wholesale into the event props, so `workspaceSlug`, `tabId` and the absolute `windowUrl` shipped with every report despite the stated "bucketed path only" constraint. Fixed at both ends: the slug and tab id are no longer put into the route context at all (nothing else read them), the sanitizer constructs its result explicitly so a stale renderer's payload can't reintroduce them, `windowUrl` is dropped since it is an install path that can carry the OS username and the bucketed route already says which page it was, and the event props are now assembled field by field so a future context key cannot ship itself. The flush moved into `freeze-flush.ts` to make both behaviours testable: `onCaptured` does not ack, the grace window does, a cancelled window keeps the breadcrumb, and props built from a context still carrying slug/tabId/windowUrl contain none of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(diagnostics): reduce MUL-5345 to route attribution only Scope call from product review: the next hang should answer "which page", and nothing more. Removes the CDP stack capture, the long-animation-frame observer, the operation breadcrumb, and the read/ack delivery protocol added earlier on this branch, along with the spike-derived build note. What stays is the smallest change that gives the two existing hang events a real route. The route reporting had been dead since MUL-4127 (#4996) deleted it along with the PostHog pageview tracker: `getDiagnosticContext` in main kept reading a WeakMap nothing wrote, so a hang report carried only the asar index.html URL. `DiagnosticRouteReporter` restores the push to main — the only party alive during a true hang, which cannot ask a blocked renderer anything — and also publishes to the in-renderer watchdog, whose `location.pathname` is that same useless packaged path because the shell runs a memory router. Paths are bucketed to templates (`/:slug/issues/:id`) before publishing, and the workspace slug and tab id are not sent at all; nothing outside diagnostics read them. The sanitizer constructs its result explicitly so a renderer older than this build cannot reintroduce them, `windowUrl` is dropped because it is an install path that can carry the OS username, and the breadcrumb event props are assembled by whitelist rather than by spreading the context. Both hang events now report `path` under the same name, so they group in one query. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): bucket hang routes by known structure, not id shape (MUL-5345) The bucketer guessed which segments were ids by looking at them — UUID, issue key, or all digits. Every id that does not look like one therefore travelled to telemetry intact, and most of ours do not: project, autopilot, agent, member, squad, runtime, skill and attachment ids are arbitrary strings from `paths.ts`. `/acme/projects/p1` bucketed to `/:slug/projects/p1`, and `/acme/runtimes/machine%2Fruntime/runtime/runtime%20one` came through completely unchanged. It now matches structurally against the known route shapes, so a `:param` slot is whatever occupies that position regardless of how it is spelled or encoded. Where several patterns fit, the most literal one wins, which keeps `agents/new` the create page rather than an agent whose id happens to be "new". An unmatched path is masked (`/:slug/issues/*`, `/:slug/*`) rather than passed through: a route we do not know is exactly the case where an id cannot be told from a page name, so nothing from it travels. That makes a route added to paths.ts without being added here a loss of detail instead of a leak. To stop the list falling behind quietly, a parity test walks the real path builders — not a copy of them — and asserts that no builder leaks its slug or ids and that none falls back to the mask. Removing a single route from the table fails it with the builder named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>