Skip to content

Commit fa5f822

Browse files
sanil-23claude
andauthored
feat(subconscious): stabilize heartbeat + subconscious loop (tinyhumansai#392) (tinyhumansai#437)
* feat(subconscious): stabilize heartbeat + subconscious loop (tinyhumansai#392) - Enable heartbeat by default (enabled=true, inference_enabled=true, 5min interval) - Seed system tasks on engine init, not first tick - SQLite-backed task/log/escalation persistence - Overlap guard with generation counter — stale ticks are cancelled - Single log entry per task per tick, updated in place (in_progress → act/noop/escalate/failed/cancelled) - Rate-limit retry (429 only) for agentic-v1 cloud model calls - Approval gate: unsolicited write actions on read-only tasks require user approval - Analysis-only mode for agentic-v1 on read-only escalations - Non-blocking status RPC — reads from DB, never blocks on engine mutex - Frontend: system vs user task distinction, toggle switches, expandable activity log - Frontend: 3s auto-poll on Subconscious tab, skill-related escalation navigation - Consecutive failure counter in status (resets on success) - last_tick_at only advances on successful evaluation - Missing LLM evaluation fallback — unevaluated tasks default to noop - Docs: subconscious.md architecture guide, memory-sync-functions.md reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix Prettier formatting for subconscious frontend files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: retrigger checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(heartbeat): use disabled config in run_returns_immediately_when_disabled test HeartbeatConfig::default() has enabled: true, so run() entered the infinite loop and never returned — hanging the test (and CI) indefinitely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(subconscious): remove HEARTBEAT.md task import, use SQLite as sole task source Tasks are now managed exclusively in SQLite via the Subconscious UI. HEARTBEAT.md is retained for instructions/context only, not as a task list. Situation report now reads pending tasks from SQLite instead of HEARTBEAT.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt on subconscious engine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1ca4ea0 commit fa5f822

19 files changed

Lines changed: 4027 additions & 966 deletions

File tree

app/src/hooks/useSubconscious.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/**
2+
* useSubconscious — hook for the subconscious engine UI.
3+
*
4+
* Provides tasks, escalations, execution log, and actions for the
5+
* subconscious tab on the Intelligence page.
6+
*/
7+
import { useCallback, useEffect, useRef, useState } from 'react';
8+
9+
import {
10+
isTauri,
11+
subconsciousEscalationsApprove,
12+
subconsciousEscalationsDismiss,
13+
subconsciousEscalationsList,
14+
subconsciousLogList,
15+
subconsciousStatus,
16+
subconsciousTasksAdd,
17+
subconsciousTasksList,
18+
subconsciousTasksRemove,
19+
subconsciousTasksUpdate,
20+
subconsciousTrigger,
21+
} from '../utils/tauriCommands';
22+
import type {
23+
SubconsciousEscalation,
24+
SubconsciousLogEntry,
25+
SubconsciousStatus,
26+
SubconsciousTask,
27+
} from '../utils/tauriCommands/subconscious';
28+
29+
export interface UseSubconsciousResult {
30+
// Data
31+
tasks: SubconsciousTask[];
32+
escalations: SubconsciousEscalation[];
33+
logEntries: SubconsciousLogEntry[];
34+
status: SubconsciousStatus | null;
35+
36+
// Loading states
37+
loading: boolean;
38+
triggering: boolean;
39+
40+
// Actions
41+
refresh: () => Promise<void>;
42+
triggerTick: () => Promise<void>;
43+
addTask: (title: string) => Promise<void>;
44+
removeTask: (taskId: string) => Promise<void>;
45+
toggleTask: (taskId: string, enabled: boolean) => Promise<void>;
46+
approveEscalation: (escalationId: string) => Promise<void>;
47+
dismissEscalation: (escalationId: string) => Promise<void>;
48+
49+
// Error
50+
error: string | null;
51+
}
52+
53+
export function useSubconscious(): UseSubconsciousResult {
54+
const [tasks, setTasks] = useState<SubconsciousTask[]>([]);
55+
const [escalations, setEscalations] = useState<SubconsciousEscalation[]>([]);
56+
const [logEntries, setLogEntries] = useState<SubconsciousLogEntry[]>([]);
57+
const [status, setStatus] = useState<SubconsciousStatus | null>(null);
58+
const [loading, setLoading] = useState(false);
59+
const [triggering, setTriggering] = useState(false);
60+
const [error, setError] = useState<string | null>(null);
61+
const fetchingRef = useRef(false);
62+
63+
const refresh = useCallback(async () => {
64+
if (!isTauri() || fetchingRef.current) return;
65+
fetchingRef.current = true;
66+
setLoading(true);
67+
setError(null);
68+
try {
69+
const [tasksRes, escalationsRes, logRes, statusRes] = await Promise.all([
70+
subconsciousTasksList().catch(() => null),
71+
subconsciousEscalationsList('pending').catch(() => null),
72+
subconsciousLogList(undefined, 30).catch(() => null),
73+
subconsciousStatus().catch(() => null),
74+
]);
75+
76+
if (tasksRes) setTasks(unwrap(tasksRes) ?? []);
77+
if (escalationsRes) setEscalations(unwrap(escalationsRes) ?? []);
78+
if (logRes) setLogEntries(unwrap(logRes) ?? []);
79+
if (statusRes) setStatus(unwrap(statusRes) ?? null);
80+
} catch (err) {
81+
setError(err instanceof Error ? err.message : 'Failed to load subconscious data');
82+
} finally {
83+
setLoading(false);
84+
fetchingRef.current = false;
85+
}
86+
}, []);
87+
88+
const triggerTick = useCallback(async () => {
89+
if (!isTauri() || triggering) return;
90+
setTriggering(true);
91+
try {
92+
await subconsciousTrigger();
93+
} catch (err) {
94+
console.warn('[subconscious] trigger failed:', err);
95+
} finally {
96+
setTriggering(false);
97+
}
98+
}, [triggering]);
99+
100+
const addTask = useCallback(
101+
async (title: string) => {
102+
if (!isTauri()) return;
103+
try {
104+
await subconsciousTasksAdd(title);
105+
await refresh();
106+
} catch (err) {
107+
console.warn('[subconscious] add task failed:', err);
108+
throw err;
109+
}
110+
},
111+
[refresh]
112+
);
113+
114+
const removeTask = useCallback(
115+
async (taskId: string) => {
116+
if (!isTauri()) return;
117+
try {
118+
await subconsciousTasksRemove(taskId);
119+
await refresh();
120+
} catch (err) {
121+
console.warn('[subconscious] remove task failed:', err);
122+
}
123+
},
124+
[refresh]
125+
);
126+
127+
const toggleTask = useCallback(
128+
async (taskId: string, enabled: boolean) => {
129+
if (!isTauri()) return;
130+
try {
131+
await subconsciousTasksUpdate(taskId, { enabled });
132+
await refresh();
133+
} catch (err) {
134+
console.warn('[subconscious] toggle task failed:', err);
135+
}
136+
},
137+
[refresh]
138+
);
139+
140+
const approveEscalation = useCallback(
141+
async (escalationId: string) => {
142+
if (!isTauri()) return;
143+
try {
144+
await subconsciousEscalationsApprove(escalationId);
145+
await refresh();
146+
} catch (err) {
147+
console.warn('[subconscious] approve failed:', err);
148+
throw err;
149+
}
150+
},
151+
[refresh]
152+
);
153+
154+
const dismissEscalation = useCallback(
155+
async (escalationId: string) => {
156+
if (!isTauri()) return;
157+
try {
158+
await subconsciousEscalationsDismiss(escalationId);
159+
await refresh();
160+
} catch (err) {
161+
console.warn('[subconscious] dismiss failed:', err);
162+
}
163+
},
164+
[refresh]
165+
);
166+
167+
// Poll every 3s while the hook is mounted (user is on Subconscious tab).
168+
// Picks up all state changes: in_progress → act/noop/escalate/failed,
169+
// new escalations, background tick completions, etc.
170+
useEffect(() => {
171+
refresh();
172+
const interval = setInterval(refresh, 3000);
173+
return () => clearInterval(interval);
174+
}, [refresh]);
175+
176+
return {
177+
tasks,
178+
escalations,
179+
logEntries,
180+
status,
181+
loading,
182+
triggering,
183+
refresh,
184+
triggerTick,
185+
addTask,
186+
removeTask,
187+
toggleTask,
188+
approveEscalation,
189+
dismissEscalation,
190+
error,
191+
};
192+
}
193+
194+
/**
195+
* Unwrap a CommandResponse — callCoreRpc returns `{ result: T, logs: [...] }`.
196+
*/
197+
function unwrap<T>(response: unknown): T | null {
198+
if (!response || typeof response !== 'object') return null;
199+
const r = response as Record<string, unknown>;
200+
// CommandResponse shape: { result: T, logs: string[] }
201+
if ('result' in r) {
202+
return r.result as T;
203+
}
204+
return null;
205+
}

0 commit comments

Comments
 (0)