Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions dashboard/api-server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,33 @@ const ALLOWED_ORIGINS = [
"http://localhost:18789", // Gateway (Swift wrapper loads dashboard here)
"http://127.0.0.1:18789", // Gateway (alternate)
];

// Argent Lite: on a Pi the operator may access via hostname or LAN IP.
// Dynamically add the system hostname and all non-loopback IPs so CORS
// doesn't block saves from the same machine via a different address.
try {
const os = require("os");
const hostname = os.hostname();
const ifaces = os.networkInterfaces();
const extraHosts = new Set([hostname]);
for (const name of Object.keys(ifaces)) {
for (const iface of ifaces[name] || []) {
if (!iface.internal && iface.family === "IPv4") {
extraHosts.add(iface.address);
}
}
}
for (const host of extraHosts) {
for (const port of [8080, 5173]) {
const origin = `http://${host}:${port}`;
if (!ALLOWED_ORIGINS.includes(origin)) {
ALLOWED_ORIGINS.push(origin);
}
}
}
Comment on lines +50 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add .local hostname aliases so Pi hostname access is truly covered.

Very strong direction overall, but right now only os.hostname() is allowlisted. On many Pi/LAN setups the browser origin is http://<hostname>.local:<port>, which can still fail CORS.

Proposed patch
   const hostname = os.hostname();
   const ifaces = os.networkInterfaces();
-  const extraHosts = new Set([hostname]);
+  const extraHosts = new Set([hostname]);
+  if (hostname && !hostname.includes(".")) {
+    extraHosts.add(`${hostname}.local`);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hostname = os.hostname();
const ifaces = os.networkInterfaces();
const extraHosts = new Set([hostname]);
for (const name of Object.keys(ifaces)) {
for (const iface of ifaces[name] || []) {
if (!iface.internal && iface.family === "IPv4") {
extraHosts.add(iface.address);
}
}
}
for (const host of extraHosts) {
for (const port of [8080, 5173]) {
const origin = `http://${host}:${port}`;
if (!ALLOWED_ORIGINS.includes(origin)) {
ALLOWED_ORIGINS.push(origin);
}
}
}
const hostname = os.hostname();
const ifaces = os.networkInterfaces();
const extraHosts = new Set([hostname]);
if (hostname && !hostname.includes(".")) {
extraHosts.add(`${hostname}.local`);
}
for (const name of Object.keys(ifaces)) {
for (const iface of ifaces[name] || []) {
if (!iface.internal && iface.family === "IPv4") {
extraHosts.add(iface.address);
}
}
}
for (const host of extraHosts) {
for (const port of [8080, 5173]) {
const origin = `http://${host}:${port}`;
if (!ALLOWED_ORIGINS.includes(origin)) {
ALLOWED_ORIGINS.push(origin);
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/api-server.cjs` around lines 50 - 67, The CORS allowlist currently
adds only os.hostname() and IPs to extraHosts before building origins; add
".local" aliases for the Pi hostname so browser origins like
http://hostname.local:8080 are allowed. Update the logic that builds extraHosts
(using the hostname variable and extraHosts Set) to also push hostname +
".local" (and optionally hostname + ".local." if you want trailing dot) into
extraHosts before the loop that creates origins, then continue adding the
constructed origins to ALLOWED_ORIGINS exactly as done now (refer to hostname,
extraHosts, and ALLOWED_ORIGINS in the diff).

} catch {
/* best-effort — loopback origins always work */
}
app.use(
cors({
origin: (origin, callback) => {
Expand Down
68 changes: 68 additions & 0 deletions dashboard/docs/pi-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Pi Profile — low-intensity AEVP mode

The ArgentOS dashboard's AEVP (Argent Emotional Visualization Presence)
renders the orb and particles via WebGL at the browser's native refresh
rate. On a Raspberry Pi 5 with software rendering (or a Hailo-free
display), the 180-particle default + 60+ fps loop can drive the CPU
above 70% and saturate the GPU queue.

**Pi Profile** is an opt-in rendering mode that reduces the per-frame
cost without changing any identity preset or personality modulation.
The orb still breathes, still colour-shifts, still emits particles on
tool events — it just costs ~60% less to draw.

## What it changes

| Lever | Default | Pi profile | Source of truth |
|---|---|---|---|
| Particle cap (runtime) | up to `180` | `60` | `aevp/colorMapping.ts` via `getMaxParticlesCap()` |
| Density scale | `1.0 ×` | `0.5 ×` | `aevp/colorMapping.ts` via `getDensityScale()` |
| Render tick interval | `0 ms` (rAF) | `33 ms` (~30 fps) | `aevp/renderer.ts` via `getFrameIntervalMs()` |

The hard allocation (`MAX_PARTICLES = 180` in `aevp/particles.ts`) is
unchanged — the buffer still holds room for 180 — so the profile can
be flipped on and off without a reload or re-alloc.
Comment on lines +22 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docs conflict on reload behavior — tighten this up.

Lines 22-24 say no reload is needed, but Lines 28-29 and 38 say activation is computed once and requires reload. Pick one behavior and state it consistently.

Also applies to: 28-29, 38-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/docs/pi-profile.md` around lines 22 - 24, The docs contradict
whether toggling the profile requires reload; pick the correct behavior (either
"no reload needed because buffer sized by MAX_PARTICLES in aevp/particles.ts" or
"activation is computed once and requires reload") and make all mentions
consistent: update the sentences around the hard allocation note and the
activation wording so they all state the chosen behavior, reference
MAX_PARTICLES in aevp/particles.ts when explaining the no-reload case, and
remove or rewrite the lines that claim activation is computed once (the earlier
"requires reload" wording) so the three locations (current lines ~22-24, ~28-29
and ~38) all match.


## How to enable

Any one of these signals activates the profile. The check runs once
at module load.

| Method | Where | When to use |
|---|---|---|
| `PI_PROFILE=1` env var | Node / SSR | Build-time or Electron wrappers |
| `VITE_PI_PROFILE=1` env var | Vite dev/build | `.env.local` in dashboard dev |
| `localStorage.setItem("argent.piProfile", "1")` | Browser console | Runtime toggle, survives reload |
| `?piProfile=1` URL param | Any | One-shot test without touching storage |

To disable, unset / remove / set to `"0"`, and reload.

## How to verify

1. Open the dashboard with the profile active.
2. Open dev tools → Performance → record ~5 seconds.
3. Expect: ~30 fps main thread, GPU frame time below 8 ms, particle
count ≤60 in the `drawArrays(POINTS, ...)` call in `renderer.ts`.
4. Compare against a baseline tab without the profile.

## What it does NOT change

- Identity presets in `identityPresets.ts` — still emitted as-is.
- Personality modulation (warmth, energy, openness, formality) — still
applied in full.
- Morning particles and evening fireflies (separate components) — mount
them conditionally in app code if those also need to drop on Pi.
- Orb bloom / glow post-processing — future cut, currently always on.

## Future cuts

- Gate bloom FBO pass behind the profile (skip `bloomFBO` render).
- Lower the shader precision on the particle fragment shader to
`mediump` when the profile is active.
- Expose an in-dashboard toggle under a "Performance" tab.

## References

- `dashboard/src/aevp/pi-profile.ts` — single source of truth.
- `dashboard/src/aevp/colorMapping.ts:448` — cap + scale call sites.
- `dashboard/src/aevp/renderer.ts:269` — frame gate.
9 changes: 8 additions & 1 deletion dashboard/src/aevp/colorMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type { EmotionalState, ActivityStateName } from "../types/agentState";
import type { AgentVisualIdentity, AEVPRenderState } from "./types";
import { classifyTool, getResonanceTargets } from "./toolCategories";
import { getMaxParticlesCap, getDensityScale } from "./pi-profile";

// ── Helpers ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -445,7 +446,13 @@ export function computeRenderState(
2.5,
);

const maxParticles = Math.round(100 * presence.particleDensity);
// Pi profile: scale density + apply a hard cap on top of identity preset.
const piCap = getMaxParticlesCap();
const piScale = getDensityScale();
const maxParticles = Math.min(
piCap,
Math.round(100 * presence.particleDensity * piScale),
);
Comment on lines +449 to +455
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pi hard cap can still be exceeded after this block.

Nice cap calculation here, but Line 486 later clamps to 100, not maxParticles. With high personality energy, Pi mode can rise above 60 again.

🔧 Cap-preserving fix
-  particleCount = Math.round(clamp(particleCount * (0.7 + p.energy * 0.6), 3, 100));
+  particleCount = Math.round(clamp(particleCount * (0.7 + p.energy * 0.6), 3, maxParticles));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/src/aevp/colorMapping.ts` around lines 449 - 455, The pi-mode cap
calculation sets maxParticles using getMaxParticlesCap(), getDensityScale(), and
presence.particleDensity but later code clamps to a hard 100 instead of using
maxParticles, allowing Pi to exceed the intended cap; update the later clamp to
use maxParticles (or Math.min(maxParticles, 100) if a secondary safety cap is
desired) wherever the particle count or Pi level is clamped so the computed
piCap and density scaling are actually enforced for functions/variables like
maxParticles and the value derived from presence.particleDensity.

let particleCount = Math.round(
clamp(
maxParticles * (0.3 + moodVis.brightness * 0.4 + arousal * 0.3) * energyMul,
Expand Down
95 changes: 95 additions & 0 deletions dashboard/src/aevp/pi-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Pi Profile — low-intensity rendering mode for the Raspberry Pi or any
* underpowered host running the ArgentOS dashboard.
*
* Activation: set any ONE of
* 1. env var PI_PROFILE=1 (SSR / build time via Vite)
* 2. env var VITE_PI_PROFILE=1 (Vite convention)
* 3. localStorage "argent.piProfile" = "1" (runtime toggle, no rebuild)
* 4. URL param ?piProfile=1 (one-shot try)
*
* Effect: the three AEVP perf levers all get tightened:
* - MAX particle count cap 180 → 60 (colorMapping runtime)
* - Runtime density multiplier 1.0 → 0.5 (colorMapping runtime)
* - Render tick frame interval 0 → 33ms (renderer ~30fps)
*
* This keeps identity presets and personality modulation intact — the
* character of the orb is preserved, only the per-frame cost drops.
* Expected reduction: ~65% fewer GPU particle updates, ~50% fewer draw
* calls on a display running at 60+ Hz.
*/

function envFlag(name: string): boolean {
try {
// Node / SSR
const p = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;
if (p?.env?.[name] && p.env[name] !== "0" && p.env[name] !== "") return true;
} catch {
/* ignore */
}
try {
// Vite (import.meta.env is statically available only inside Vite-processed
// modules; protect with a typeof check so this file compiles under raw tsc)
const meta = (globalThis as { __ARGENT_VITE_ENV__?: Record<string, string | undefined> })
.__ARGENT_VITE_ENV__;
if (meta?.[name] && meta[name] !== "0" && meta[name] !== "") return true;
} catch {
Comment on lines +30 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for __ARGENT_VITE_ENV__ definitions/usages..."
rg -n --hidden -C 2 '__ARGENT_VITE_ENV__|defineProperty\(.+__ARGENT_VITE_ENV__|window\.__ARGENT_VITE_ENV__|globalThis\.__ARGENT_VITE_ENV__'

echo
echo "Searching for VITE_PI_PROFILE usage patterns..."
rg -n --hidden -C 2 'VITE_PI_PROFILE|import\.meta\.env'

Repository: ArgentAIOS/argentos-core

Length of output: 5494


VITE_PI_PROFILE activation path is totally broken — that custom global doesn't exist.

Lines 33–35 reach for __ARGENT_VITE_ENV__, but it's never injected anywhere in the code. Huge problem. Meanwhile, every other file in the dashboard correctly reads import.meta.env directly. This activation method is completely dead.

Fix it to use import.meta.env like all the rest of the application does:

🔧 Correct env read
-    const meta = (globalThis as { __ARGENT_VITE_ENV__?: Record<string, string | undefined> })
-      .__ARGENT_VITE_ENV__;
-    if (meta?.[name] && meta[name] !== "0" && meta[name] !== "") return true;
+    const viteEnv =
+      typeof import.meta !== "undefined"
+        ? ((import.meta as { env?: Record<string, string | undefined> }).env ?? undefined)
+        : undefined;
+    if (viteEnv?.[name] && viteEnv[name] !== "0" && viteEnv[name] !== "") return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
// Vite (import.meta.env is statically available only inside Vite-processed
// modules; protect with a typeof check so this file compiles under raw tsc)
const meta = (globalThis as { __ARGENT_VITE_ENV__?: Record<string, string | undefined> })
.__ARGENT_VITE_ENV__;
if (meta?.[name] && meta[name] !== "0" && meta[name] !== "") return true;
} catch {
try {
// Vite (import.meta.env is statically available only inside Vite-processed
// modules; protect with a typeof check so this file compiles under raw tsc)
const viteEnv =
typeof import.meta !== "undefined"
? ((import.meta as { env?: Record<string, string | undefined> }).env ?? undefined)
: undefined;
if (viteEnv?.[name] && viteEnv[name] !== "0" && viteEnv[name] !== "") return true;
} catch {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/src/aevp/pi-profile.ts` around lines 30 - 36, The code currently
reads a non-existent custom global __ARGENT_VITE_ENV__ (referenced as meta and
checked via meta?.[name]) which breaks VITE_PI_PROFILE activation; replace that
lookup with the standard Vite environment access using import.meta.env (e.g.
read import.meta.env[name] or (import.meta as any).env[name]) wherever meta or
__ARGENT_VITE_ENV__ is used so the check (and the same guards for "0" and empty
string) works like other dashboard modules; update any try/catch around that
block to reflect the direct import.meta.env access and remove the custom global
references.

/* ignore */
}
return false;
}

function localStorageFlag(key: string): boolean {
try {
const ls = (globalThis as { localStorage?: { getItem(k: string): string | null } }).localStorage;
const v = ls?.getItem(key);
return v === "1" || v === "true";
} catch {
return false;
}
}

function urlParamFlag(key: string): boolean {
try {
const loc = (globalThis as { location?: { search?: string } }).location;
if (!loc?.search) return false;
const params = new URLSearchParams(loc.search);
const v = params.get(key);
return v === "1" || v === "true";
} catch {
return false;
}
}

/** True if any Pi-profile signal is active. Computed once at module load. */
export const PI_PROFILE_ACTIVE: boolean =
envFlag("PI_PROFILE") ||
envFlag("VITE_PI_PROFILE") ||
localStorageFlag("argent.piProfile") ||
urlParamFlag("piProfile");

/**
* Hard runtime cap on active particle count. Colour mapping applies this
* in addition to identity preset density so low-end hardware gets a
* predictable ceiling regardless of mood.
*/
export function getMaxParticlesCap(): number {
return PI_PROFILE_ACTIVE ? 60 : 180;
}

/**
* Multiplicative scale applied to presence.particleDensity at runtime.
* Identity presets still express themselves (relative density is preserved),
* but the absolute count lands lower.
*/
export function getDensityScale(): number {
return PI_PROFILE_ACTIVE ? 0.5 : 1.0;
}

/**
* Minimum milliseconds between tick renders. Zero means no gate (use
* requestAnimationFrame cadence). 33 ≈ 30 fps, 50 = 20 fps.
*/
export function getFrameIntervalMs(): number {
return PI_PROFILE_ACTIVE ? 33 : 0;
}
7 changes: 7 additions & 0 deletions dashboard/src/aevp/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import type { AEVPRenderState } from "./types";
import { getFrameIntervalMs } from "./pi-profile";
import {
ParticleSystem,
type ParticleFormationRequest,
Expand Down Expand Up @@ -272,6 +273,12 @@ export class AEVPRenderer {

if (this.contextLost) return;

// Pi profile: throttle ticks to >= frameIntervalMs between renders.
const frameInterval = getFrameIntervalMs();
if (frameInterval > 0 && now - this.lastFrameTime < frameInterval) {
return;
}

const dt = Math.min((now - this.lastFrameTime) / 1000, 0.1); // Cap at 100ms
this.lastFrameTime = now;

Expand Down
15 changes: 7 additions & 8 deletions dashboard/src/components/SetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,12 @@ export function SetupWizard({ isOpen, onComplete }: SetupWizardProps) {
className={`w-5 h-5 ${authType === "setup-token" ? "text-amber-400" : "text-white/40"}`}
/>
<div>
<div className="text-white font-medium">Claude Setup Token</div>
<div className="text-white font-medium">Claude API Key</div>
<div className="text-white/40 text-xs mt-0.5">
From Anthropic Max subscription. Run{" "}
From{" "}
<code className="text-amber-400/70 bg-white/5 px-1 rounded">
claude setup-token
</code>{" "}
in terminal.
console.anthropic.com/settings/keys
</code>
Comment on lines +360 to +365
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

UI says “API Key,” but this path still behaves like legacy setup-token flow.

Lines 360/490/504/669 rebrand setup-token as API key, but the selected mode still serializes as type: "token" in save logic. That mismatch can store the wrong auth profile shape.

🔧 Minimal alignment fix
-              setAuthType("setup-token");
+              setAuthType("api-key");
-                {authType === "setup-token"
-                  ? "Claude API Key"
+                {authType === "api-key"
+                  ? "Claude API Key"
                   : authType === "minimax-key"

Also applies to: 489-505, 669-670

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dashboard/src/components/SetupWizard.tsx` around lines 360 - 365, The UI in
SetupWizard shows "Claude API Key" but the save logic still serializes the
Anthropic/Claude auth profile with type: "token", causing a mismatch; update the
serialization in the save handler inside the SetupWizard component (the function
that builds/saves the Anthropic auth/profile object) so that when the UI mode is
the API Key variant it emits the matching type (e.g., "apiKey") and the correct
shape (field names matching the backend contract), and propagate that change to
the other save/serialize spots mentioned (the other blocks that currently emit
type: "token") or alternatively revert the label to "Setup token" so UI and
serialized type remain consistent. Ensure validation and any downstream
consumers expect the new type/shape.

</div>
</div>
</div>
Expand Down Expand Up @@ -488,7 +487,7 @@ export function SetupWizard({ isOpen, onComplete }: SetupWizardProps) {
<div>
<label className="text-white/60 text-xs font-medium block mb-1">
{authType === "setup-token"
? "Setup Token"
? "Claude API Key"
: authType === "minimax-key"
? "MiniMax API Key"
: authType === "glm-key"
Expand All @@ -502,7 +501,7 @@ export function SetupWizard({ isOpen, onComplete }: SetupWizardProps) {
onChange={(e) => setToken(e.target.value)}
placeholder={
authType === "setup-token"
? "sk-ant-oat01-..."
? "sk-ant-api03-..."
: authType === "minimax-key"
? "sk-cp-... or eyJ..."
: authType === "glm-key"
Expand Down Expand Up @@ -667,7 +666,7 @@ export function SetupWizard({ isOpen, onComplete }: SetupWizardProps) {
authType === "skip"
? "Skipped (configure in Settings)"
: authType === "setup-token"
? `Setup token saved as ${profileName}`
? `Claude API key saved as ${profileName}`
: `API key saved as ${profileName}`;

const modelName =
Expand Down
1 change: 1 addition & 0 deletions dashboard/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default defineConfig({
server: {
port: 8080,
host: true,
allowedHosts: true,
cors: {
origin: true,
credentials: true,
Expand Down
Loading