Skip to content

Aria AI#3275

Open
Zer0-deu wants to merge 6 commits into
koala73:mainfrom
Zer0-deu:main
Open

Aria AI#3275
Zer0-deu wants to merge 6 commits into
koala73:mainfrom
Zer0-deu:main

Conversation

@Zer0-deu
Copy link
Copy Markdown

@Zer0-deu Zer0-deu commented Apr 21, 2026

Aria implemented

Summary

Type of change

  • Bug fix
  • New feature
  • New data source / feed
  • New map layer
  • Refactor / code cleanup
  • Documentation
  • CI / Build / Infrastructure

Affected areas

  • Map / Globe
  • News panels / RSS feeds
  • AI Insights / World Brief
  • Market Radar / Crypto
  • Desktop app (Tauri)
  • API endpoints (/api/*)
  • Config / Settings
  • Other:

Checklist

  • Tested on worldmonitor.app variant
  • Tested on tech.worldmonitor.app variant (if applicable)
  • New RSS feed domains added to api/rss-proxy.js allowlist (if adding feeds)
  • No API keys or secrets committed
  • TypeScript compiles without errors (npm run typecheck)

Screenshots

Aria implemented
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

@Zer0-deu is attempting to deploy a commit to the World Monitor Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the trust:caution Brin: contributor trust score caution label Apr 21, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This PR introduces ARIA, a JARVIS-like AI assistant panel that streams LLM responses over SSE, assembles context from 30+ data sources, and integrates into the WorldMonitor panel grid. Several P1 bugs remain across the server handler, client panel, and service layer in addition to those already flagged in the prior review thread.

  • assembleAriaContext domain filtering corrupts context keys: after deleting non-requested entries from contextPromises, Object.values() positional destructuring assigns results to the wrong variable names, feeding the LLM incorrect data (e.g. cyber metrics labelled as MARKETS).
  • Conversation history is one-sided: saveConversation is only called with userMessage; ARIA's streamed reply is never persisted, so the chat history rendered on reload shows only user questions.
  • Missing auth on panel fetch: AriaPanel.handleQuery uses a raw fetch without credentials while the handler calls validateApiKey, unlike ChatAnalystPanel which uses premiumFetch.

Confidence Score: 3/5

Not safe to merge — multiple P1 defects produce wrong LLM context, broken conversation persistence, and likely auth failures on every query.

Between the issues in the prior review thread (SSE parsing, response.ok missing, hardcoded awareness data, active_alerts dead branch) and the three new P1s found here (domain-filter destructuring misalignment, missing aria-message save, unauthenticated fetch), the feature's primary user path has at least five confirmed P1 defects still present.

server/worldmonitor/aria/v1/handler.ts and src/components/AriaPanel.ts require the most attention.

Important Files Changed

Filename Overview
server/worldmonitor/aria/v1/handler.ts Core server handler with multiple P1 bugs: domain-filter destructuring misalignment corrupts LLM context, active_alerts ternary always returns [], and handleGetAwareness ignores the assembled live context in favour of fully static hardcoded data.
src/components/AriaPanel.ts UI panel with three P1 issues: raw fetch sends no auth credentials, ARIA responses are never saved to localStorage (history is one-sided after reload), and the inline SSE data: line lookup bug (flagged in prior review thread) still produces silent data loss for multi-event chunks.
src/services/aria/index.ts Client-side service with correct SSE streaming pattern (index-based data-line lookup), but getAriaAlerts return type is Promise<void> while the body returns response.body (a ReadableStream), and /api/aria/stream-alerts has no corresponding route in api/aria.js.
api/aria.js Thin Vercel Edge router that correctly dispatches to handler functions with auth; no /stream-alerts route is defined, making getAriaAlerts always 404.
src/config/panels.ts Adds aria panel entry (premium: false) to the full-variant panel registry; no issues.
src/app/app-context.ts Adds ariaAwareness and enableAria optional properties to AppContext; straightforward and correct.
src/app/data-loader.ts Wires getCachedAwarenessState into the parallel load pipeline behind a shouldLoad('aria') guard with proper error handling; no issues.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant AP as AriaPanel.ts
    participant API as api/aria.js (Edge)
    participant H as handler.ts
    participant LLM as LLM (callLlmReasoningStream)
    participant Cache as Redis / cachedFetchJson

    U->>AP: Type query + press QUERY
    AP->>API: POST /api/aria/query (no auth header ⚠️)
    API->>H: handleAriaQuery(request, env)
    H->>H: validateApiKey → 401 if fails ⚠️
    H->>Cache: assembleAriaContext() — fetches 8 data sources
    Note over H: domain filter may corrupt<br/>destructuring order ⚠️
    H->>LLM: callLlmReasoningStream(systemPrompt, query)
    LLM-->>H: stream chunks
    H-->>API: SSE: event:metadata / event:delta / event:action / event:done
    API-->>AP: SSE stream (response.ok not checked ⚠️)
    AP->>AP: Parse SSE — lines.find always returns first data: line ⚠️
    AP->>AP: Update UI with delta content
    AP->>AP: saveConversation(userMessage only) ⚠️
    Note over AP: ariaMessage never saved to localStorage
Loading

Reviews (2): Last reviewed commit: "Merge branch 'koala73:main' into main" | Re-trigger Greptile

Comment on lines +415 to +455
for (const line of lines.slice(0, -1)) {
if (line.startsWith("event:")) {
const eventType = line.substring(7).trim();
const nextIdx = lines.indexOf(lines.find((l) => l.startsWith("data:")) || "");
if (nextIdx > -1) {
const dataLine = lines[nextIdx];
if (dataLine.startsWith("data:")) {
const data = JSON.parse(dataLine.substring(6));

if (eventType === "metadata") {
if (!ariaMessage) {
ariaMessage = {
id: data.conversation_id,
role: "aria",
content: "",
timestamp: new Date(),
metadata: {
mode: data.mode,
sources: data.accessed_domains,
},
};
this.addMessage(messagesContainer, ariaMessage);
}
} else if (eventType === "delta" && ariaMessage) {
ariaMessage.content += data.delta;
this.updateLastMessage(messagesContainer, ariaMessage);
} else if (eventType === "action") {
if (!ariaMessage) ariaMessage = {
id: data.id || `aria-${Date.now()}`,
role: "aria",
content: "",
timestamp: new Date(),
};
if (!ariaMessage.actions) ariaMessage.actions = [];
ariaMessage.actions.push(data);
this.updateLastMessage(messagesContainer, ariaMessage);
}
}
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Broken SSE data-line lookup and missing response error check

lines.find((l) => l.startsWith("data:")) always searches the whole lines array and returns the first data: line regardless of which event is being processed. When multiple SSE events arrive in the same chunk (e.g. metadata + delta), every event ends up reading the same first data line, so delta content is silently discarded. Compare the correct index-based approach used in src/services/aria/index.ts:92.

Additionally, there is no response.ok check before consuming the body (line 403). A 401 or 500 error body is not SSE-formatted, so the inner JSON.parse silently catches and swallows every line, leaving the user with a hidden typing indicator and no ARIA response.

Comment on lines +258 to +273
export async function getAriaAlerts(
severity?: "info" | "warning" | "critical"
): Promise<void> {
const params = new URLSearchParams();
if (severity) {
params.set("severity", severity);
}

const response = await fetch(`/api/aria/stream-alerts?${params.toString()}`);

if (!response.ok) {
throw new Error(`Failed to get alerts: ${response.status}`);
}

// Handle WebSocket/SSE streaming
return response.body;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Return type mismatch in getAriaAlerts

The function is typed as Promise<void> but returns response.body (a ReadableStream<Uint8Array> | null). TypeScript will accept this silently because the return value is untyped in the body, but any caller expecting void will receive a stream object they cannot use. The comment says "Handle WebSocket/SSE streaming" suggesting the intended signature should expose the stream.

},
]
: [],
active_alerts: includeAlerts ? [] : [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dead conditional — active_alerts always returns []

Both branches of the ternary produce the same empty array, so the includeAlerts query parameter has no effect. When includeAlerts is false the intent is clearly to omit the field; the current code ignores that intent.

Comment on lines +363 to +416

const awareness = {
as_of: new Date().toISOString(),
focus_areas: [
{
domain: "military",
region: "Eastern Europe",
topic: "Conflict Escalation",
intensity: 0.85,
status: "active",
},
{
domain: "markets",
region: "Global",
topic: "Volatility Spike",
intensity: 0.65,
status: "monitoring",
},
{
domain: "climate",
region: "Southeast Asia",
topic: "Severe Weather",
intensity: 0.75,
status: "developing",
},
],
recent_events: includeRecentEvents
? [
{
occurred_at: new Date(Date.now() - 3600000).toISOString(),
title: "Major Market Move",
description: "20% volatility spike in global markets",
affected_domains: ["markets", "economic"],
affected_regions: ["Global"],
impact_score: 0.8,
},
]
: [],
active_alerts: includeAlerts ? [] : [],
trending: includeTrending
? [
{
topic: "AI Regulation",
category: "policy",
momentum: 0.72,
mention_count: 1247,
related_topics: ["tech", "governance"],
},
]
: [],
system_confidence: 0.92,
data_freshness: 0.88,
sources_connected: 28,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 handleGetAwareness returns fully hardcoded data

The assembled context from assembleAriaContext is fetched but never used in the response — focus_areas, recent_events, trending, system_confidence, and sources_connected are all static literals. Clients relying on this endpoint for real awareness state will always receive the same canned response regardless of actual live data.

@Zer0-deu Zer0-deu marked this pull request as draft April 21, 2026 19:26
@Zer0-deu Zer0-deu marked this pull request as ready for review April 21, 2026 19:40
Comment on lines +98 to +115
if (domains.length > 0) {
for (const [key] of Object.entries(contextPromises)) {
if (!domains.includes(key)) {
delete contextPromises[key];
}
}
}

const [
markets,
military,
climate,
cyber,
economic,
maritime,
aviation,
news,
] = await Promise.all(Object.values(contextPromises).map((p) => p.catch(() => null)));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Domain filter silently corrupts context destructuring

When the domains filter removes keys from contextPromises, Object.values() returns only the surviving entries in their original insertion order. The subsequent positional destructuring still expects all 8 values in the fixed order [markets, military, climate, cyber, economic, maritime, aviation, news]. So if a caller passes domains: ["cyber"], Object.values returns [cyberPromise], and the destructuring binds that result to markets while cyber gets undefined. The LLM prompt will then contain cyber data labelled as MARKETS and report CYBER: No data — silently feeding wrong information to the model for every filtered query.

The fix is to resolve the full set of promises unconditionally and then zero out keys the caller didn't request, or use named resolution (e.g. Promise.allSettled with a key-preserving map):

const results = await Promise.allSettled(
  Object.entries(contextPromises).map(async ([k, p]) => [k, await p.catch(() => null)])
);
const resolved = Object.fromEntries(results.map(r => r.status === 'fulfilled' ? r.value : []));

Comment on lines +469 to +471
if (typingIndicator) typingIndicator.style.display = "none";
this.saveConversation(userMessage);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 ARIA responses never persisted to localStorage

After streaming completes, saveConversation(userMessage) is called but ariaMessage (which accumulated the full streamed reply) is never saved. On the next page load, loadConversation() will render a history of one-sided user messages with no ARIA responses. Since saveConversation appends to the stored array, ARIA's reply also needs to be saved once the stream is done:

    if (typingIndicator) typingIndicator.style.display = "none";
    this.saveConversation(userMessage);
    if (ariaMessage) this.saveConversation(ariaMessage);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trust:caution Brin: contributor trust score caution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant