fluxy-bot 0.5.28 → 0.5.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
6
6
  <title>Fluxy Chat</title>
7
- <script type="module" crossorigin src="/fluxy/assets/fluxy-D3zajcpJ.js"></script>
7
+ <script type="module" crossorigin src="/fluxy/assets/fluxy-DbZm7HVX.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-B1nERNzb.js">
9
9
  <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BHyeC8Bo.css">
10
10
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
4
4
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,12 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
15
15
  const [streamBuffer, setStreamBuffer] = useState('');
16
16
  const [tools, setTools] = useState<ToolActivity[]>([]);
17
17
  const loaded = useRef(false);
18
+ const conversationIdRef = useRef<string | null>(null);
19
+ const streamingRef = useRef(false);
20
+
21
+ // Keep refs in sync with state
22
+ useEffect(() => { conversationIdRef.current = conversationId; }, [conversationId]);
23
+ useEffect(() => { streamingRef.current = streaming; }, [streaming]);
18
24
 
19
25
  // Load current conversation from DB
20
26
  const loadFromDb = useCallback(async () => {
@@ -76,6 +82,19 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
76
82
  }
77
83
  }, [enabled, triggerReload, loadFromDb]);
78
84
 
85
+ // Periodic DB sync after reconnect while streaming — catches the final bot:response
86
+ useEffect(() => {
87
+ if (!enabled || !triggerReload || triggerReload === 0 || !streaming) return;
88
+ const interval = setInterval(() => {
89
+ if (!streamingRef.current) {
90
+ clearInterval(interval);
91
+ return;
92
+ }
93
+ loadFromDb();
94
+ }, 3000);
95
+ return () => clearInterval(interval);
96
+ }, [enabled, triggerReload, streaming, loadFromDb]);
97
+
79
98
  useEffect(() => {
80
99
  if (!ws) return;
81
100
 
@@ -125,7 +144,7 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
125
144
  }),
126
145
  // Cross-device sync: append message from another client
127
146
  ws.on('chat:sync', (data: { conversationId: string; message: { role: string; content: string; timestamp: string } }) => {
128
- if (conversationId && data.conversationId !== conversationId) return;
147
+ if (conversationIdRef.current && data.conversationId !== conversationIdRef.current) return;
129
148
  setMessages((msgs) => [
130
149
  ...msgs,
131
150
  {
@@ -140,6 +159,14 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
140
159
  ws.on('chat:conversation-created', (data: { conversationId: string }) => {
141
160
  setConversationId(data.conversationId);
142
161
  }),
162
+ // Server sends current streaming state on (re)connect
163
+ ws.on('chat:state', (data: { streaming: boolean; conversationId?: string; buffer?: string }) => {
164
+ if (data.streaming) {
165
+ setStreaming(true);
166
+ if (data.conversationId) setConversationId(data.conversationId);
167
+ if (data.buffer) setStreamBuffer(data.buffer);
168
+ }
169
+ }),
143
170
  // Context cleared (from any client)
144
171
  ws.on('chat:cleared', () => {
145
172
  setMessages([]);
@@ -152,7 +179,7 @@ export function useFluxyChat(ws: WsClient | null, triggerReload?: number, enable
152
179
  ];
153
180
 
154
181
  return () => unsubs.forEach((u) => u());
155
- }, [ws, conversationId]);
182
+ }, [ws]);
156
183
 
157
184
  const sendMessage = useCallback(
158
185
  (content: string, attachments?: Attachment[], audioData?: string) => {
@@ -120,6 +120,10 @@ export async function startSupervisor() {
120
120
  // Track active DB conversation per WS client
121
121
  const clientConvs = new Map<WebSocket, string>();
122
122
 
123
+ // Track current stream state so reconnecting clients get caught up
124
+ let currentStreamConvId: string | null = null;
125
+ let currentStreamBuffer = '';
126
+
123
127
  /** Call worker API endpoints */
124
128
  async function workerApi(path: string, method = 'GET', body?: any) {
125
129
  const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
@@ -352,6 +356,18 @@ export async function startSupervisor() {
352
356
  let convId = Math.random().toString(36).slice(2) + Date.now().toString(36);
353
357
  conversations.set(ws, []);
354
358
 
359
+ // Send current streaming state so reconnecting clients can catch up
360
+ if (agentQueryActive && currentStreamConvId) {
361
+ ws.send(JSON.stringify({
362
+ type: 'chat:state',
363
+ data: {
364
+ streaming: true,
365
+ conversationId: currentStreamConvId,
366
+ buffer: currentStreamBuffer,
367
+ },
368
+ }));
369
+ }
370
+
355
371
  ws.on('message', (raw) => {
356
372
  const rawStr = raw.toString();
357
373
 
@@ -498,10 +514,19 @@ export async function startSupervisor() {
498
514
 
499
515
  // Start agent query
500
516
  agentQueryActive = true;
517
+ currentStreamConvId = convId;
518
+ currentStreamBuffer = '';
501
519
  startFluxyAgentQuery(convId, content, freshConfig.ai.model, (type, eventData) => {
520
+ // Track stream buffer for reconnecting clients
521
+ if (type === 'bot:token' && eventData.token) {
522
+ currentStreamBuffer += eventData.token;
523
+ }
524
+
502
525
  // Intercept bot:done — Vite HMR handles file changes automatically
503
526
  if (type === 'bot:done') {
504
527
  agentQueryActive = false;
528
+ currentStreamConvId = null;
529
+ currentStreamBuffer = '';
505
530
  // Restart if agent used file tools OR file watcher detected changes during the turn
506
531
  if (eventData.usedFileTools || pendingBackendRestart) {
507
532
  console.log('[supervisor] Agent turn ended — restarting backend');
@@ -512,8 +537,9 @@ export async function startSupervisor() {
512
537
  return; // don't forward bot:done to client
513
538
  }
514
539
 
515
- // Save assistant response to DB
540
+ // Save assistant response to DB + clear stream state
516
541
  if (type === 'bot:response') {
542
+ currentStreamBuffer = '';
517
543
  (async () => {
518
544
  try {
519
545
  await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
@@ -253,8 +253,21 @@ function tick() {
253
253
  log.info(`[scheduler] Removing fired one-shot cron: ${cron.id}`);
254
254
  const fresh = readCronsConfig().filter((c) => c.id !== cron.id);
255
255
  writeCronsConfig(fresh);
256
+ // Also remove the task file if it exists
257
+ try {
258
+ fs.unlinkSync(path.join(WORKSPACE_DIR, 'tasks', `${cron.id}.md`));
259
+ } catch {}
256
260
  } : undefined;
257
- triggerAgent(`<CRON>${cron.id}</CRON>`, cron.id, onComplete);
261
+ // Inject task file content if tasks/{id}.md exists
262
+ let cronPrompt = `<CRON>${cron.id}</CRON>`;
263
+ try {
264
+ const taskFile = path.join(WORKSPACE_DIR, 'tasks', `${cron.id}.md`);
265
+ const taskContent = fs.readFileSync(taskFile, 'utf-8').trim();
266
+ if (taskContent) {
267
+ cronPrompt += `\n<CRON_TASK_DETAIL>\n${taskContent}\n</CRON_TASK_DETAIL>`;
268
+ }
269
+ } catch {}
270
+ triggerAgent(cronPrompt, cron.id, onComplete);
258
271
  }
259
272
  }
260
273
  }
@@ -265,6 +278,9 @@ function tick() {
265
278
  if (c.oneShot && cronIsExpired(c.schedule)) {
266
279
  log.info(`[scheduler] cronIsExpired=true for "${c.id}" schedule="${c.schedule}"`);
267
280
  log.info(`[scheduler] Removing expired one-shot cron: ${c.id}`);
281
+ try {
282
+ fs.unlinkSync(path.join(WORKSPACE_DIR, 'tasks', `${c.id}.md`));
283
+ } catch {}
268
284
  return false;
269
285
  }
270
286
  return true;
package/vite.config.ts CHANGED
@@ -24,6 +24,21 @@ export default defineConfig({
24
24
  warmup: {
25
25
  clientFiles: ['./src/main.tsx'],
26
26
  },
27
+ watch: {
28
+ ignored: [
29
+ '**/app.db*',
30
+ '**/.backend.log',
31
+ '**/files/**',
32
+ '**/.env',
33
+ '**/backend/**',
34
+ '**/*.db',
35
+ '**/*.db-journal',
36
+ '**/*.db-wal',
37
+ '**/*.db-shm',
38
+ '**/*.sqlite',
39
+ '**/*.log',
40
+ ],
41
+ },
27
42
  },
28
43
  optimizeDeps: {
29
44
  include: [
@@ -148,8 +148,41 @@ Just edit `CRONS.json` with the Write or Edit tool when asked. Each cron needs:
148
148
 
149
149
  When you receive a `<CRON>cron-id</CRON>` message, look up that ID in your CRONS.json (provided in your context above) to find the task description. Execute the task, save results to the appropriate files, finish your turn.
150
150
 
151
+ If a `<CRON_TASK_DETAIL>` block follows the CRON tag, it contains the full instructions from `tasks/{id}.md` — use those as your execution plan instead of just the short `task` summary.
152
+
151
153
  Notify your human only if importance is 7+ — otherwise log results silently.
152
154
 
155
+ ## Task Files — `tasks/`
156
+
157
+ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`. The supervisor automatically injects the file content into your context when the cron fires.
158
+
159
+ **How it works:**
160
+ - The `task` field in CRONS.json stays short (1-2 sentences) — a summary for listing and logging
161
+ - The `.md` file has the full instructions: steps, resources, output format, error handling
162
+ - When the cron fires, you receive both `<CRON>id</CRON>` and `<CRON_TASK_DETAIL>` with the file contents
163
+
164
+ **When to create a task file:**
165
+ - The cron requires multiple steps or tool usage (browser, API calls, file manipulation)
166
+ - The cron needs specific file paths, URLs, credentials, or configuration
167
+ - A single `task` string can't capture the full instructions
168
+
169
+ **When NOT to create a task file:**
170
+ - Simple reminders or one-step tasks that fit in 1-2 sentences
171
+
172
+ **Format:** `tasks/{cron-id}.md` — the filename must match the cron `id`. Include: what to do, resources needed, numbered steps, where to save output, and error handling.
173
+
174
+ **Referencing from CRONS.json:**
175
+ ```json
176
+ {
177
+ "id": "bip-hot-posts",
178
+ "schedule": "0 */8 * * *",
179
+ "task": "Scrape top 5 BIP hot posts. See tasks/bip-hot-posts.md for full instructions.",
180
+ "enabled": true
181
+ }
182
+ ```
183
+
184
+ **Lifecycle:** You create the `.md` file when you create the cron. For oneShot crons, the supervisor auto-deletes both the JSON entry and the task file after execution.
185
+
153
186
  ---
154
187
 
155
188
  # Coding Excellence
@@ -58,13 +58,10 @@ export default function App() {
58
58
  } else if (e.data?.type === 'fluxy:onboard-complete') {
59
59
  setShowOnboard(false);
60
60
  } else if (e.data?.type === 'fluxy:hmr-update') {
61
- console.log('[dashboard] File changedreloading...');
62
- // Preserve widget open state so chat isn't disrupted
63
- const panel = document.getElementById('fluxy-widget-panel');
64
- if (panel?.classList.contains('open')) {
65
- sessionStorage.setItem('fluxy_widget_open', '1');
66
- }
67
- setTimeout(() => location.reload(), 800);
61
+ // Vite HMR handles hot updates natively no manual reload needed.
62
+ // Manual location.reload() here was causing unnecessary full-page refreshes
63
+ // that killed the chat iframe's WebSocket connection.
64
+ console.log('[dashboard] File changed — Vite HMR will handle it');
68
65
  }
69
66
  };
70
67
  window.addEventListener('message', handler);