Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0857c98
refactor(subconscious): replace task evaluator with agent-per-tick model
senamakel May 31, 2026
c1780f6
refactor(ui): simplify subconscious tab for agent-per-tick model
senamakel May 31, 2026
0df4a8b
fix: update remaining test files for agent-per-tick model
senamakel May 31, 2026
e60a3d5
style: apply prettier + cargo fmt formatting
senamakel May 31, 2026
9b76fef
Merge remote-tracking branch 'upstream/main' into pr/3079
senamakel Jun 1, 2026
845a215
feat(subconscious): wire up real agent harness with tool access
senamakel Jun 1, 2026
776fca2
feat(subconscious): add tiered SubconsciousMode (off/simple/aggressive)
senamakel Jun 1, 2026
03b4676
feat(ui): add tiered mode selector to subconscious tab
senamakel Jun 1, 2026
a656b56
fix(subconscious): add subconscious_mode to heartbeat RPC schema
senamakel Jun 1, 2026
06c82c7
feat(ui): add frequency slider to subconscious tab
senamakel Jun 1, 2026
b296a03
fix(inference): surface actionable error when Managed route fails wit…
senamakel-droid Jun 1, 2026
aa91c66
fix(memory/safety): exclude bare-phone patterns from strict PII rejec…
oxoxDev Jun 1, 2026
ea1a5b8
test: green Rust Core Coverage (stale assertions + env-race serializa…
sanil-23 Jun 1, 2026
8a6a253
fix(composio): complete connection-disconnect cleanup (config entry +…
sanil-23 Jun 1, 2026
de85d1f
feat(analytics): wire product event tracking (#3123)
senamakel Jun 2, 2026
cb4b897
refactor(agent): unwire eager 7-day memory-tree digest from turn loop…
sanil-23 Jun 2, 2026
0257f09
fix: resolve merge conflict in subconscious store.rs
senamakel Jun 2, 2026
68ecb69
fix: address CodeRabbit review feedback
senamakel Jun 2, 2026
ad85d00
test: add useSubconscious hook tests for coverage gate
senamakel Jun 2, 2026
461d28b
fix: add missing afterEach import in useSubconscious test
senamakel Jun 2, 2026
3e33e12
style: format useSubconscious test with prettier
senamakel Jun 2, 2026
bfd73ee
fix(subconscious): track agent failures separately from empty ticks
senamakel Jun 2, 2026
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
416 changes: 5 additions & 411 deletions app/src/components/intelligence/IntelligenceSubconsciousTab.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ export default function SubconsciousReflectionCards({
)}
</div>
<div className="flex flex-col gap-2 flex-shrink-0">
{r.thread_id && (
<button
data-testid={`reflection-view-${r.id}`}
onClick={() => onNavigateToThread?.(r.thread_id!)}
className="px-3 py-1.5 text-xs bg-stone-50 dark:bg-neutral-800/60 hover:bg-stone-100 dark:hover:bg-neutral-800 border border-stone-200 dark:border-neutral-700 text-stone-600 dark:text-neutral-300 rounded-lg transition-colors">
{t('reflections.viewConversation')}
</button>
)}
{r.proposed_action && (
<button
data-testid={`reflection-act-${r.id}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
/**
* Vitest for the Intelligence Subconscious tab (#623).
* Vitest for the Intelligence Subconscious tab.
*
* Covers `handleNavigateToReflectionThread` — the callback passed to
* `SubconsciousReflectionCards`. The function is small but load-bearing:
* it dispatches `setSelectedThread(threadId)` so `Conversations` resumes
* the new thread on mount, then routes to `/chat` (the unified chat
* surface; `/conversations` redirects to `/home`). Both dispatch and
* navigate are mocked so we can assert the contract without spinning up
* the full Redux/router stack.
* Covers navigation from reflection cards and provider unavailable state.
*/
import { fireEvent, render, screen } from '@testing-library/react';
import type { ComponentProps } from 'react';
Expand All @@ -23,10 +17,6 @@ vi.mock('react-redux', () => ({ useDispatch: () => mockDispatch, useSelector: ()

vi.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate }));

// Stub out the cards component so we can trigger the navigate callback
// directly without exercising the RPC / polling path (already covered by
// `SubconsciousReflectionCards.test.tsx`). The stub renders a button
// that fires `onNavigateToThread` with a known thread id when clicked.
vi.mock('../SubconsciousReflectionCards', () => ({
default: ({ onNavigateToThread }: { onNavigateToThread?: (id: string) => void }) => (
<button
Expand All @@ -38,25 +28,8 @@ vi.mock('../SubconsciousReflectionCards', () => ({
),
}));

function baseProps() {
return {
addSubconsciousTask: vi.fn(),
approveEscalation: vi.fn(),
dismissEscalation: vi.fn(),
expandedLogIds: new Set<string>(),
logEntries: [],
newTaskTitle: '',
removeSubconsciousTask: vi.fn(),
setExpandedLogIds: vi.fn(),
setNewTaskTitle: vi.fn(),
status: null as ComponentProps<typeof IntelligenceSubconsciousTab>['status'],
tasks: [],
toggleSubconsciousTask: vi.fn(),
triggerTick: vi.fn(),
triggering: false,
escalations: [],
loading: false,
};
function baseProps(): ComponentProps<typeof IntelligenceSubconsciousTab> {
return { status: null, triggerTick: vi.fn(), triggering: false };
}

describe('IntelligenceSubconsciousTab', () => {
Expand All @@ -71,13 +44,7 @@ describe('IntelligenceSubconsciousTab', () => {
it('on Act → dispatches setSelectedThread + navigates to /chat', () => {
render(<IntelligenceSubconsciousTab {...baseProps()} />);
fireEvent.click(screen.getByTestId('cards-stub-trigger'));
// Redux dispatch payload should match the slice's action creator
// exactly — comparing the produced action keeps the assertion robust
// if the slice path changes.
expect(mockDispatch).toHaveBeenCalledWith(setSelectedThread('spawned-thread-42'));
// Route must be `/chat` (the unified chat surface), not
// `/conversations` — the latter falls through to a `/home` redirect
// and the user lands somewhere unexpected.
expect(mockNavigate).toHaveBeenCalledWith('/chat');
});

Expand All @@ -94,8 +61,6 @@ describe('IntelligenceSubconsciousTab', () => {
interval_minutes: 5,
last_tick_at: null,
total_ticks: 0,
task_count: 3,
pending_escalations: 0,
consecutive_failures: 1,
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function refl(overrides: Partial<Reflection> = {}): Reflection {
created_at: 1,
acted_on_at: null,
dismissed_at: null,
thread_id: null,
...overrides,
};
}
Expand Down
170 changes: 6 additions & 164 deletions app/src/hooks/useSubconscious.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,24 @@
/**
* useSubconscious — hook for the subconscious engine UI.
*
* Provides tasks, escalations, execution log, and actions for the
* subconscious tab on the Intelligence page.
* Provides status, thoughts (reflections), and engine control actions
* for the subconscious tab on the Intelligence page.
*/
import { useCallback, useEffect, useRef, useState } from 'react';

import {
isTauri,
subconsciousEscalationsApprove,
subconsciousEscalationsDismiss,
subconsciousEscalationsList,
subconsciousLogList,
subconsciousStatus,
subconsciousTasksAdd,
subconsciousTasksList,
subconsciousTasksRemove,
subconsciousTasksUpdate,
subconsciousTrigger,
} from '../utils/tauriCommands';
import type {
SubconsciousEscalation,
SubconsciousLogEntry,
SubconsciousStatus,
SubconsciousTask,
} from '../utils/tauriCommands/subconscious';
import { isTauri, subconsciousStatus, subconsciousTrigger } from '../utils/tauriCommands';
import type { SubconsciousStatus } from '../utils/tauriCommands/subconscious';

export interface UseSubconsciousResult {
// Data
tasks: SubconsciousTask[];
escalations: SubconsciousEscalation[];
logEntries: SubconsciousLogEntry[];
status: SubconsciousStatus | null;

// Loading states
loading: boolean;
triggering: boolean;

// Actions
refresh: () => Promise<void>;
triggerTick: () => Promise<void>;
addTask: (title: string) => Promise<void>;
removeTask: (taskId: string) => Promise<void>;
toggleTask: (taskId: string, enabled: boolean) => Promise<void>;
approveEscalation: (escalationId: string) => Promise<void>;
dismissEscalation: (escalationId: string) => Promise<void>;

// Error
error: string | null;
}

export function useSubconscious(): UseSubconsciousResult {
const [tasks, setTasks] = useState<SubconsciousTask[]>([]);
const [escalations, setEscalations] = useState<SubconsciousEscalation[]>([]);
const [logEntries, setLogEntries] = useState<SubconsciousLogEntry[]>([]);
const [status, setStatus] = useState<SubconsciousStatus | null>(null);
const [loading, setLoading] = useState(false);
const [triggering, setTriggering] = useState(false);
Expand All @@ -66,24 +31,7 @@ export function useSubconscious(): UseSubconsciousResult {
setLoading(true);
setError(null);
try {
// Each RPC is bounded by RPC_TIMEOUT_MS so Promise.all is guaranteed
// to settle. Without this, a single hung request (e.g. sidecar held
// in a long-running tick) would leave fetchingRef.current === true
// forever, and every subsequent 3s poll would silently no-op at the
// early-return above — freezing the Intelligence page on a stale
// snapshot. withTimeout returns null on timeout, matching the
// existing `.catch(() => null)` failure contract, so downstream
// setState calls just skip that slice for this tick.
const [tasksRes, escalationsRes, logRes, statusRes] = await Promise.all([
withTimeout(subconsciousTasksList()),
withTimeout(subconsciousEscalationsList('pending')),
withTimeout(subconsciousLogList(undefined, 30)),
withTimeout(subconsciousStatus()),
]);

if (tasksRes) setTasks(unwrap(tasksRes) ?? []);
if (escalationsRes) setEscalations(unwrap(escalationsRes) ?? []);
if (logRes) setLogEntries(unwrap(logRes) ?? []);
const statusRes = await withTimeout(subconsciousStatus());
if (statusRes) setStatus(unwrap(statusRes) ?? null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load subconscious data');
Expand All @@ -105,82 +53,6 @@ export function useSubconscious(): UseSubconsciousResult {
}
}, [triggering]);

const addTask = useCallback(
async (title: string) => {
if (!isTauri()) return;
try {
await subconsciousTasksAdd(title);
await refresh();
} catch (err) {
console.warn('[subconscious] add task failed:', err);
throw err;
}
},
[refresh]
);

const removeTask = useCallback(
async (taskId: string) => {
if (!isTauri()) return;
try {
await subconsciousTasksRemove(taskId);
await refresh();
} catch (err) {
console.warn('[subconscious] remove task failed:', err);
}
},
[refresh]
);

const toggleTask = useCallback(
async (taskId: string, enabled: boolean) => {
if (!isTauri()) return;
try {
await subconsciousTasksUpdate(taskId, { enabled });
await refresh();
} catch (err) {
console.warn('[subconscious] toggle task failed:', err);
}
},
[refresh]
);

const approveEscalation = useCallback(
async (escalationId: string) => {
if (!isTauri()) return;
try {
await subconsciousEscalationsApprove(escalationId);
await refresh();
} catch (err) {
console.warn('[subconscious] approve failed:', err);
throw err;
}
},
[refresh]
);

const dismissEscalation = useCallback(
async (escalationId: string) => {
if (!isTauri()) return;
try {
await subconsciousEscalationsDismiss(escalationId);
await refresh();
} catch (err) {
console.warn('[subconscious] dismiss failed:', err);
}
},
[refresh]
);

// Poll every 3s while the hook is mounted (user is on Subconscious tab).
// Picks up all state changes: in_progress → act/noop/escalate/failed,
// new escalations, background tick completions, etc.
//
// On unmount we also clear fetchingRef — otherwise a request that times
// out or resolves after the component has been torn down would leave the
// ref stuck `true` for the next mount (React Strict Mode double-mount in
// dev, or tab navigation back to Intelligence), silently wedging the
// poller exactly as before.
useEffect(() => {
refresh();
const interval = setInterval(refresh, 3000);
Expand All @@ -190,51 +62,21 @@ export function useSubconscious(): UseSubconsciousResult {
};
}, [refresh]);

return {
tasks,
escalations,
logEntries,
status,
loading,
triggering,
refresh,
triggerTick,
addTask,
removeTask,
toggleTask,
approveEscalation,
dismissEscalation,
error,
};
return { status, loading, triggering, refresh, triggerTick, error };
}

/**
* Per-RPC client-side timeout for the polling refresh. Must be strictly
* less than the 3s poll interval so a hung call can't stack up across
* ticks. 2500ms leaves a 500ms safety margin.
*/
const RPC_TIMEOUT_MS = 2500;

/**
* Race a promise against a timeout. Resolves to `null` on timeout or
* rejection — matching the prior `.catch(() => null)` contract used by
* the refresh logic so downstream code can treat "no data this tick" and
* "RPC failed this tick" identically.
*/
function withTimeout<T>(promise: Promise<T>, ms: number = RPC_TIMEOUT_MS): Promise<T | null> {
return Promise.race<T | null>([
promise.catch(() => null),
new Promise<null>(resolve => setTimeout(() => resolve(null), ms)),
]);
}

/**
* Unwrap a CommandResponse — callCoreRpc returns `{ result: T, logs: [...] }`.
*/
function unwrap<T>(response: unknown): T | null {
if (!response || typeof response !== 'object') return null;
const r = response as Record<string, unknown>;
// CommandResponse shape: { result: T, logs: string[] }
if ('result' in r) {
return r.result as T;
}
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'الإجراء المقترح',
'reflections.act': 'تنفيذ',
'reflections.dismiss': 'تجاهل',
'reflections.viewConversation': 'عرض',
'whatsapp.chatsSynced': 'محادثات مزامنة',
'whatsapp.chatSynced': 'محادثة مزامنة',
'sync.active': 'نشط',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'প্রস্তাবিত কাজ',
'reflections.act': 'কাজ করুন',
'reflections.dismiss': 'বাদ দিন',
'reflections.viewConversation': 'দেখুন',
'whatsapp.chatsSynced': 'চ্যাট সিঙ্ক হয়েছে',
'whatsapp.chatSynced': 'চ্যাট সিঙ্ক হয়েছে',
'sync.active': 'সক্রিয়',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'Vorgeschlagene Aktion',
'reflections.act': 'Handeln',
'reflections.dismiss': 'Entlassen',
'reflections.viewConversation': 'Ansehen',
'whatsapp.chatsSynced': 'Chats synchronisiert',
'whatsapp.chatSynced': 'Chat synchronisiert',
'sync.active': 'Aktiv',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2119,6 +2119,7 @@ const en: TranslationMap = {
'reflections.proposedAction': 'Proposed Action',
'reflections.act': 'Act',
'reflections.dismiss': 'Dismiss',
'reflections.viewConversation': 'View',

// WhatsApp
'whatsapp.chatsSynced': 'chats synced',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1959,6 +1959,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'Acción propuesta',
'reflections.act': 'Actuar',
'reflections.dismiss': 'Descartar',
'reflections.viewConversation': 'Ver',
Comment thread
senamakel marked this conversation as resolved.
'whatsapp.chatsSynced': 'chats sincronizados',
'whatsapp.chatSynced': 'chat sincronizado',
'sync.active': 'Activo',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'Action proposée',
'reflections.act': 'Agir',
'reflections.dismiss': 'Ignorer',
'reflections.viewConversation': 'Voir',
'whatsapp.chatsSynced': 'conversations synchronisées',
'whatsapp.chatSynced': 'conversation synchronisée',
'sync.active': 'Actif',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'प्रस्तावित एक्शन',
'reflections.act': 'करें',
'reflections.dismiss': 'हटाएं',
'reflections.viewConversation': 'देखें',
Comment thread
senamakel marked this conversation as resolved.
'whatsapp.chatsSynced': 'चैट्स सिंक हुईं',
'whatsapp.chatSynced': 'चैट सिंक हुई',
'sync.active': 'एक्टिव',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1922,6 +1922,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'Tindakan yang Diusulkan',
'reflections.act': 'Tindakan',
'reflections.dismiss': 'Abaikan',
'reflections.viewConversation': 'Lihat',
'whatsapp.chatsSynced': 'obrolan disinkronkan',
'whatsapp.chatSynced': 'obrolan disinkronkan',
'sync.active': 'Aktif',
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,7 @@ const messages: TranslationMap = {
'reflections.proposedAction': 'Azione proposta',
'reflections.act': 'Agisci',
'reflections.dismiss': 'Ignora',
'reflections.viewConversation': 'Visualizza',
'whatsapp.chatsSynced': 'chat sincronizzate',
'whatsapp.chatSynced': 'chat sincronizzata',
'sync.active': 'Attivo',
Expand Down
Loading
Loading