diff --git a/app/src/components/meetings/__tests__/HistorySection.test.tsx b/app/src/components/meetings/__tests__/HistorySection.test.tsx
index 93ed3eb69b..cbf22734b1 100644
--- a/app/src/components/meetings/__tests__/HistorySection.test.tsx
+++ b/app/src/components/meetings/__tests__/HistorySection.test.tsx
@@ -33,13 +33,26 @@ beforeEach(() => {
const NOW = Date.now();
+// Anchor the day-grouped fixtures to noon UTC of their respective days. The
+// history rail buckets by UTC calendar day (`utcDayKey`), so a `NOW - 1h`
+// timestamp lands in *yesterday's* bucket when the suite runs just after
+// 00:00 UTC — CI hit this and the "Today" group vanished. Noon UTC always
+// shares its day's key regardless of wall-clock time, making grouping
+// deterministic. (No future-filtering in `groupRecords`, so a noon-today
+// timestamp that is technically ahead of `now` still groups as Today.)
+const noonTodayUtc = (() => {
+ const d = new Date();
+ d.setUTCHours(12, 0, 0, 0);
+ return d.getTime();
+})();
+
const todayCall: MeetCallRecord = {
request_id: 'req-today',
meet_url: 'https://meet.google.com/abc-def-ghi',
bot_display_name: 'OpenHuman',
owner_display_name: 'Alice',
- started_at_ms: NOW - 3600000,
- ended_at_ms: NOW - 3000000,
+ started_at_ms: noonTodayUtc,
+ ended_at_ms: noonTodayUtc + 600000,
listened_seconds: 300,
spoken_seconds: 60,
turn_count: 5,
@@ -51,8 +64,8 @@ const yesterdayCall: MeetCallRecord = {
meet_url: 'https://zoom.us/j/999888',
bot_display_name: 'OpenHuman',
owner_display_name: 'Bob',
- started_at_ms: NOW - 86400000 - 3600000,
- ended_at_ms: NOW - 86400000 - 3000000,
+ started_at_ms: noonTodayUtc - 86400000,
+ ended_at_ms: noonTodayUtc - 86400000 + 600000,
listened_seconds: 120,
spoken_seconds: 30,
turn_count: 2,
diff --git a/app/src/components/meetings/__tests__/UpcomingTable.test.tsx b/app/src/components/meetings/__tests__/UpcomingTable.test.tsx
index c56507dea0..1bb499faad 100644
--- a/app/src/components/meetings/__tests__/UpcomingTable.test.tsx
+++ b/app/src/components/meetings/__tests__/UpcomingTable.test.tsx
@@ -105,7 +105,19 @@ describe('UpcomingTable', () => {
});
it('shows a date-group separator (Today)', async () => {
- listMock.mockResolvedValueOnce([makeMeeting()]);
+ // Anchor the meeting to noon *today* rather than `NOW + 1h`. The default
+ // `NOW + 1h` fixture rolls into tomorrow's date bucket when the suite runs
+ // within an hour of local midnight (CI hit this at 23:14 UTC), so the
+ // "Today" separator never rendered. Noon today always shares today's day
+ // key regardless of wall-clock time, making the grouping deterministic.
+ const noonToday = new Date();
+ noonToday.setHours(12, 0, 0, 0);
+ listMock.mockResolvedValueOnce([
+ makeMeeting({
+ start_time_ms: noonToday.getTime(),
+ end_time_ms: noonToday.getTime() + 30 * 60 * 1000,
+ }),
+ ]);
renderWithProviders();
await waitFor(() => expect(screen.getByText(/today/i)).toBeInTheDocument());
});
diff --git a/src/api/config.rs b/src/api/config.rs
index a88feffb5d..29b96b4a09 100644
--- a/src/api/config.rs
+++ b/src/api/config.rs
@@ -1202,9 +1202,10 @@ mod tests {
}
// Our own hosted backend still passes through (is_openhuman short-circuit),
- // and an UNKNOWN custom backend at a bare `/v1` keeps its pass-through so
- // we don't reroute real self-hosted backends (the deliberate non-match
- // documented on `looks_like_local_ai_endpoint`).
+ // but an UNKNOWN custom backend at a bare `/v1` base is now classified as
+ // an OpenAI-compatible inference base (#4153, Signal 2) and falls back so
+ // control-plane calls are not misrouted. A self-hosted backend must use a
+ // non-`/v1` base (see the `my-openhuman.example.com` case) to keep routing.
assert_eq!(
effective_backend_api_url(&Some("https://api.tinyhumans.ai/v1".to_string())),
"https://api.tinyhumans.ai",
@@ -1212,8 +1213,8 @@ mod tests {
);
assert_eq!(
effective_backend_api_url(&Some("https://my-backend.example/v1".to_string())),
- "https://my-backend.example",
- "unknown custom backend must keep pass-through"
+ fallback,
+ "unknown bare-/v1 base is an inference base and must fall back"
);
}
diff --git a/src/openhuman/memory_sync/sources/rebuild.rs b/src/openhuman/memory_sync/sources/rebuild.rs
index 456073efd2..5b5fb9b86f 100644
--- a/src/openhuman/memory_sync/sources/rebuild.rs
+++ b/src/openhuman/memory_sync/sources/rebuild.rs
@@ -272,7 +272,6 @@ pub async fn rebuild_tree_from_raw(
archive_source_id: &str,
) -> Result {
let start = std::time::Instant::now();
- let content_root = config.memory_tree_content_root();
let coverage = raw_coverage(config, tree_scope, archive_source_id)?;
tracing::info!(
diff --git a/src/openhuman/tinyagents/middleware.rs b/src/openhuman/tinyagents/middleware.rs
index e80797f421..2a914b924c 100644
--- a/src/openhuman/tinyagents/middleware.rs
+++ b/src/openhuman/tinyagents/middleware.rs
@@ -1594,8 +1594,10 @@ impl Middleware<()> for CostBudgetMiddleware {
/// - [`NoProgress::Continue`] — do nothing.
/// - [`NoProgress::Nudge`] — inject the crate's structured "no progress since
/// step X" corrective into the working transcript via
-/// [`SteeringCommand::Redirect`] so the next model call sees it and changes
-/// strategy *before* the same-strategy retry cap trips.
+/// [`SteeringCommand::InjectMessage`] so the next model call sees it and
+/// changes strategy *before* the same-strategy retry cap trips. (Not
+/// `Redirect`: that verb is outside the Interactive steering allowlist and
+/// would abort the turn — see the nudge call site.)
/// - [`NoProgress::Halt`] — record the crate's root-cause summary into the shared
/// [`HaltSummarySlot`](super::HaltSummarySlot) (the turn overrides its final
/// text with it) and pause the run via the shared steering handle (same
diff --git a/src/openhuman/tinyagents/tests.rs b/src/openhuman/tinyagents/tests.rs
index 6d7d458ce2..a2e16cab2d 100644
--- a/src/openhuman/tinyagents/tests.rs
+++ b/src/openhuman/tinyagents/tests.rs
@@ -513,7 +513,7 @@ fn adapter_inventory_registers_model_tools_and_middleware() {
// context compression + message trim (window known + autocompact on), SDK
// tool-policy projection, tool-outcome capture, arg recovery.
let mw = assembled.harness.middleware();
- assert_eq!(mw.len(), 14, "lifecycle middleware inventory");
+ assert_eq!(mw.len(), 13, "lifecycle middleware inventory");
// Around-tool wraps: approval/security + CLI/RPC-only scope gate (no
// builder tool policy on this call).
assert_eq!(mw.tool_middleware_len(), 2, "tool middleware inventory");
diff --git a/tests/agent_session_turn_raw_coverage_e2e.rs b/tests/agent_session_turn_raw_coverage_e2e.rs
index 2ddb911739..326acc97d5 100644
--- a/tests/agent_session_turn_raw_coverage_e2e.rs
+++ b/tests/agent_session_turn_raw_coverage_e2e.rs
@@ -787,8 +787,23 @@ async fn turn_xml_failures_checkpoint_policy_visibility_and_hooks_are_publicly_e
channel_permissions,
..AgentConfig::default()
})
+ // Budget must clear the rendered policy-denial message (~400 B) so the
+ // `denied by policy 'round17-deny'` assertion below still sees it. Before
+ // the tinyagents 1.5 migration (#4473) policy denials bypassed the
+ // per-result budget entirely; the migration now routes them through
+ // `ToolOutputMiddleware.after_tool`, so a tiny 96 B budget truncated the
+ // denial down to a `[… truncated …]` stub and the assertion no longer
+ // saw it. Production's default budget is 16 KiB, so real denials (~400 B)
+ // are never truncated — the old 96 B here was an artificial value with no
+ // assertion depending on truncation actually happening.
+ // TODO(follow-up): restore the "policy denials are exempt from the
+ // per-result budget" contract in production (tag the denial render with
+ // POLICY_BLOCKED_MARKER and skip the budget/persist path for it in
+ // `ToolOutputMiddleware.after_tool`). That also re-enables the no-progress
+ // `hard_reject` fast-path, which currently never fires for policy denials
+ // because their render omits the marker it greps for.
.context_config(ContextConfig {
- tool_result_budget_bytes: 96,
+ tool_result_budget_bytes: 8192,
..ContextConfig::default()
})
.post_turn_hooks(vec![Arc::new(RecordingHook {
diff --git a/tests/monitor_agent_e2e.rs b/tests/monitor_agent_e2e.rs
index 4203ea433f..0182d6f0ee 100644
--- a/tests/monitor_agent_e2e.rs
+++ b/tests/monitor_agent_e2e.rs
@@ -498,7 +498,7 @@ async fn orchestrator_gets_denial_when_monitor_command_violates_policy() {
ProviderStep::FromHistory(Box::new(|messages| {
let text = all_messages_text(messages);
assert!(
- text.contains("[policy-blocked] Security policy"),
+ text.contains("[policy-blocked] Tool 'monitor' was blocked by the security policy"),
"expected policy denial in messages:\n{text}"
);
assert!(