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
2 changes: 1 addition & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ modules:
- class: modules::observability::OtelModule
config:
enabled: true
exporter: stdout
exporter: memory
metrics_enabled: true
logs_enabled: true
alerts:
Expand Down
11 changes: 9 additions & 2 deletions crates/tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,15 @@ impl App {
}

fn client() -> reqwest::Client {
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(key) = std::env::var("AGENTOS_API_KEY") {
if let Ok(val) = reqwest::header::HeaderValue::from_str(&format!("Bearer {}", key)) {
headers.insert(reqwest::header::AUTHORIZATION, val);
}
}
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.default_headers(headers)
.build()
.unwrap_or_default()
}
Expand Down Expand Up @@ -525,7 +532,7 @@ impl App {
let agent_id = if self.chat_agent.is_empty() {
self.agents.first()
.and_then(|a| a["id"].as_str().or(a["name"].as_str()))
.unwrap_or("default")
.unwrap_or("brain")
.to_string()
Comment on lines 532 to 536
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

Make fallback agent configurable to avoid cross-surface regressions.

Line 535 hardcodes "brain" when no agent is selected. src/streaming.ts still defaults missing agent context to "default" (Line 24 and Line 29 in the provided snippet), so this creates fallback drift and can break chat in environments that don’t provision a brain agent.

Proposed fix
-        let agent_id = if self.chat_agent.is_empty() {
+        let agent_id = if self.chat_agent.is_empty() {
             self.agents.first()
                 .and_then(|a| a["id"].as_str().or(a["name"].as_str()))
-                .unwrap_or("brain")
+                .unwrap_or(default_chat_agent())
                 .to_string()
         } else {
             self.chat_agent.clone()
         };
fn default_chat_agent() -> &'static str {
    // Keeps legacy compatibility by default; BeeOS can override via env at deploy time.
    if std::env::var("AGENTOS_PRIMARY_AGENT").ok().as_deref() == Some("brain") {
        "brain"
    } else {
        "default"
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tui/src/main.rs` around lines 532 - 536, The hardcoded fallback
"brain" in the agent selection should be made configurable; replace the inline
"brain" default in the agent_id computation (where self.chat_agent, self.agents
are used) with a call to a helper like default_chat_agent() that reads an
environment override (e.g., AGENTOS_PRIMARY_AGENT) and returns the appropriate
static string ("brain" for legacy or "default" otherwise); update or add the
default_chat_agent() function and use it in the agent_id expression so the
fallback matches other surfaces (e.g., streaming.ts) and can be overridden at
deploy time.

} else {
self.chat_agent.clone()
Expand Down Expand Up @@ -1276,7 +1283,7 @@ fn draw_chat(f: &mut Frame, app: &App, area: Rect) {
let agent_label = if app.chat_agent.is_empty() {
app.agents.first()
.and_then(|a| a["name"].as_str())
.unwrap_or("default")
.unwrap_or("brain")
} else {
Comment on lines 1283 to 1287
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

Align chat title target resolution with send path.

Line 1284 through Line 1286 resolve only name, while send_chat resolves id or name. If an agent has id but no name, the title can incorrectly show "brain".

Proposed fix
     let agent_label = if app.chat_agent.is_empty() {
         app.agents.first()
-            .and_then(|a| a["name"].as_str())
+            .and_then(|a| a["name"].as_str().or(a["id"].as_str()))
             .unwrap_or("brain")
     } else {
         &app.chat_agent
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tui/src/main.rs` around lines 1283 - 1287, The agent_label resolution
branch currently only looks up agent["name"] when app.chat_agent is empty,
causing fallback to "brain" if an agent has an id but no name; change the lookup
in the agent_label block to mirror send_chat's logic by attempting
agent["id"].as_str().or_else(|| agent["name"].as_str()).unwrap_or("brain")
(i.e., check id first, then name, then default) so titles match the send path;
reference the agent_label variable, app.chat_agent, app.agents, and the
send_chat resolution to locate and update the code.

&app.chat_agent
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Sentrux Review Report

- Actor: `codex`
- Scope: `agentos-tsc-cleanup`
- Timestamp: `2026-04-04T02:26:09+07:00`
- Worktree: `/home/wunitb/unitb_labs/wunb-agentos-beeos/.worktrees/agentos-beeos-main-runtime`
- Base commit: `4b4de07e2603d71f87bee17a91a8185dcb82e6a7`

## Commands Used

```text
npx tsc -p tsconfig.json --noEmit --pretty false
npm test -- src/__tests__/mcp-client.test.ts src/__tests__/security-headers.test.ts src/__tests__/skillkit-bridge.test.ts src/__tests__/streaming.test.ts src/__tests__/llm-router.test.ts src/__tests__/session-lifecycle.test.ts src/__tests__/cron.test.ts src/__tests__/api.test.ts
git diff --check
Sentrux scan: /home/wunitb/unitb_labs/wunb-agentos-beeos/.worktrees/agentos-beeos-main-runtime
Comment on lines +6 to +15
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

Scrub machine-local paths before committing this report.

The Worktree and Sentrux scan entries expose /home/wunitb/..., which leaks a developer username and makes the artifact non-portable. Prefer repo-relative paths or a placeholder here.

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

In `@docs/reviews/sentrux/2026-04-04-022609-codex-agentos-tsc-cleanup.md` around
lines 6 - 15, The report exposes machine-local absolute paths in the lines
labeled "Worktree" and "Sentrux scan"; update the report to remove personal
machine paths by replacing those absolute paths with repo-relative paths or a
placeholder (e.g., ./worktree or <WORKTREE_PATH>) and ensure any other
occurrences of `/home/<user>/...` are scrubbed; edit the document content that
contains the "Worktree" and "Sentrux scan" entries so the artifact no longer
contains developer usernames or machine-specific locations.

Sentrux check_rules
```

## Findings

- Severity: medium
- Area: TypeScript compile hygiene and iii-sdk contract drift
- Evidence: `tsc` initially failed on test mock signatures, `request_format`/`response_format` shape in `src/api.ts`, `unknown` state-list returns in `src/context-cache.ts`, `src/cron.ts`, and `src/session-lifecycle.ts`, plus drift around `agentTier`, `function_id`, and `globalThis.vitest`.
- Recommended change: align local typings with current iii-sdk schema contract, keep request/response schemas wrapped in a top-level object format node, and prefer explicit narrowing/casts at `state::*` boundaries instead of relying on `any`.

## Handoff For Dev Session

- Files likely affected:
`src/api.ts`, `src/llm-router.ts`, `src/types.ts`, `src/context-cache.ts`, `src/cron.ts`, `src/session-lifecycle.ts`, `src/shared/metrics.ts`, `src/evolve.ts`, targeted test files.
- First fix to attempt:
Re-run `tsc` first whenever `iii-sdk` is upgraded, because compile errors clearly expose contract drift before runtime.
- Verification expected after fix:
`npx tsc -p tsconfig.json --noEmit --pretty false` exits 0 and the targeted vitest slice above stays green.

## MegaMemory Update

- Created concept: `agentos-runtime-typescript-contract-cleanup`
29 changes: 28 additions & 1 deletion src/__tests__/cron.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ const mockTrigger = vi.fn(async (fnId: string, data?: any): Promise<any> => {
return null;
});
const mockTriggerVoid = vi.fn();
const mockRegisterTrigger = vi.fn();

const handlers: Record<string, Function> = {};
vi.mock("iii-sdk", () => ({
registerWorker: () => ({
registerFunction: (config: any, handler: Function) => {
handlers[config.id] = handler;
},
registerTrigger: vi.fn(),
registerTrigger: mockRegisterTrigger,
trigger: (req: any) =>
req.action
? mockTriggerVoid(req.function_id, req.payload)
Expand Down Expand Up @@ -158,3 +159,29 @@ describe("cron::reset_rate_limits", () => {
expect(getScope("rates").has("expired-rate")).toBe(false);
});
});

describe("cron trigger registration", () => {
it("registers built-in cron jobs with six-field expressions", () => {
expect(mockRegisterTrigger).toHaveBeenCalledWith(
expect.objectContaining({
type: "cron",
function_id: "cron::cleanup_stale_sessions",
config: { expression: "0 0 */6 * * *" },
}),
);
expect(mockRegisterTrigger).toHaveBeenCalledWith(
expect.objectContaining({
type: "cron",
function_id: "cron::aggregate_daily_costs",
config: { expression: "0 0 * * * *" },
}),
);
expect(mockRegisterTrigger).toHaveBeenCalledWith(
expect.objectContaining({
type: "cron",
function_id: "cron::reset_rate_limits",
config: { expression: "0 */5 * * * *" },
}),
);
});
});
62 changes: 62 additions & 0 deletions src/__tests__/eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,68 @@ describe("eval::suite", () => {
call("eval::suite", authReq({ suiteId: "nope" })),
).rejects.toThrow("Suite not found");
});

it("returns suite metadata and baseline comparison context BeeOS can display", async () => {
seedKv("eval_suites", "suite-routing", {
suiteId: "suite-routing",
name: "Routing Suite",
functionId: "test::double",
metadata: {
candidateClass: "routing",
baselineFunctionId: "beeos::route-task-baseline",
},
testCases: [
{
input: { value: 2 },
expected: { result: 4 },
scorer: "exact_match",
},
],
});

const result = await call(
"eval::suite",
authReq({ suiteId: "suite-routing" }),
);

expect(result.metadata).toEqual({
candidateClass: "routing",
baselineFunctionId: "beeos::route-task-baseline",
baselineAggregate: expect.objectContaining({
testCount: 1,
}),
});
Comment on lines +243 to +272
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

This BeeOS regression test doesn't actually execute the BeeOS baseline path.

mockTrigger only returns synthetic outputs for evolved:: and test:: IDs, so beeos::route-task-baseline falls through to null. That means this assertion can still pass without proving that baseline comparison is wired correctly. Please add an explicit BeeOS baseline mock and assert at least one baseline score/value, not just testCount.

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

In `@src/__tests__/eval.test.ts` around lines 243 - 272, The test currently seeds
a suite that references baselineFunctionId "beeos::route-task-baseline" but
mockTrigger only returns outputs for "evolved::" and "test::" IDs, so the BeeOS
baseline path is not exercised; update the test's mocking setup (the mockTrigger
used in this test suite) to include a synthetic response for
"beeos::route-task-baseline" (or treat "beeos::" prefixes) that returns a
plausible baseline output with at least one score/value, then extend the
assertions after call("eval::suite") to check baselineAggregate contains that
score/value (not just testCount), referencing the mockTrigger and the suite seed
using suiteId "suite-routing" and baselineFunctionId
"beeos::route-task-baseline".

});

it("preserves weighted aggregation for candidate suite results", async () => {
seedKv("eval_suites", "weighted-suite", {
suiteId: "weighted-suite",
name: "Weighted Suite",
functionId: "test::double",
testCases: [
{
input: { value: 2 },
expected: { result: 999 },
scorer: "exact_match",
weight: 1,
},
{
input: { value: 5 },
expected: { result: 10 },
scorer: "exact_match",
weight: 3,
},
],
});

const result = await call(
"eval::suite",
authReq({ suiteId: "weighted-suite" }),
);

expect(result.aggregate.correctness).toBe(0.75);
expect(result.aggregate.passRate).toBe(0.5);
});
});

describe("eval::history", () => {
Expand Down
100 changes: 100 additions & 0 deletions src/__tests__/evolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,32 @@ describe("evolve::generate", () => {
call("evolve::generate", authReq({ goal: "test" })),
).rejects.toThrow("goal, name, and agentId are required");
});

it("preserves candidate class and rollout metadata on generated records", async () => {
const result = await call(
"evolve::generate",
authReq({
goal: "Improve routing",
name: "router_candidate",
agentId: "agent-1",
metadata: {
candidateClass: "routing",
riskLabel: "medium",
rolloutState: "draft",
rolloutHint: "shadow",
sourceObservationId: "obs-1",
},
}),
);

expect(result.metadata).toMatchObject({
candidateClass: "routing",
riskLabel: "medium",
rolloutState: "draft",
rolloutHint: "shadow",
sourceObservationId: "obs-1",
});
});
});

describe("evolve::register", () => {
Expand Down Expand Up @@ -216,6 +242,14 @@ describe("evolve::register", () => {
);
expect(result.registered).toBe(false);
expect(result.reason).toContain("Security scan failed");
expect(getScope("evolved_functions").get("evolved::bad_v1")).toEqual(
expect.objectContaining({
status: "killed",
metadata: expect.objectContaining({
rolloutState: "killed",
}),
}),
);

mockTrigger.mockImplementation(originalImpl);
});
Expand All @@ -239,6 +273,71 @@ describe("evolve::register", () => {
call("evolve::register", authReq({})),
).rejects.toThrow("functionId is required");
});

it("preserves BeeOS candidate metadata through registration while advancing rollout state", async () => {
seedKv("evolved_functions", "evolved::router_v1", {
functionId: "evolved::router_v1",
code: "async (input) => { return input; }",
description: "routing candidate",
authorAgentId: "agent-1",
version: 1,
status: "draft",
securityReport: { scanSafe: false, sandboxPassed: false, findingCount: 0 },
metadata: {
candidateClass: "routing",
riskLabel: "medium",
rolloutState: "draft",
rolloutHint: "shadow",
},
});

await call(
"evolve::register",
authReq({ functionId: "evolved::router_v1" }),
);

expect(getScope("evolved_functions").get("evolved::router_v1")).toEqual(
expect.objectContaining({
status: "staging",
metadata: expect.objectContaining({
candidateClass: "routing",
riskLabel: "medium",
rolloutState: "staging",
rolloutHint: "shadow",
}),
}),
);
});

it("marks rollout metadata killed when sandbox validation fails", async () => {
seedKv("evolved_functions", "evolved::sandbox_bad_v1", {
functionId: "evolved::sandbox_bad_v1",
code: "async () => { throw new Error('boom'); }",
description: "sandbox fail",
authorAgentId: "agent-1",
version: 1,
status: "draft",
securityReport: { scanSafe: false, sandboxPassed: false, findingCount: 0 },
metadata: {
rolloutState: "draft",
},
});

const result = await call(
"evolve::register",
authReq({ functionId: "evolved::sandbox_bad_v1" }),
);

expect(result.registered).toBe(false);
expect(getScope("evolved_functions").get("evolved::sandbox_bad_v1")).toEqual(
expect.objectContaining({
status: "killed",
metadata: expect.objectContaining({
rolloutState: "killed",
}),
}),
);
});
});

describe("evolve::unregister", () => {
Expand All @@ -258,6 +357,7 @@ describe("evolve::unregister", () => {

const stored: any = getScope("evolved_functions").get("evolved::rm_v1");
expect(stored.status).toBe("killed");
expect(stored.metadata.rolloutState).toBe("killed");
});

it("rejects non-author agents", async () => {
Expand Down
Loading