From aa80dcbaa0246298c1c6aa00103612fe5e1cf608 Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:02:51 +0000 Subject: [PATCH 1/7] feat(websocket): add echo WebSocket server and simple frontend UI - Server: - Add `ws` dependency - Attach `WebSocketServer` on path `/ws` to existing Express HTTP server - Type-safe handlers and basic welcome + echo behavior - Client: - Add WebSocket proxy in Vite for `/ws` - Implement simple WebSocket UI in App: - Connects automatically - Input + Send button - Status indicator and message log This enables a basic echo experience: - Frontend connects to `/ws` (proxied in dev, same-origin in prod) - Server echoes any message back to the client Mentat precommit script passed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/24b3f169-bec0-44a3-b7db-8ea4c36ccc44 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- .mentat/README.md | 4 +- client/src/App.tsx | 137 ++++++++++++++++++++++++++++++++++++++++-- client/vite.config.ts | 6 ++ server/package.json | 3 +- server/src/server.ts | 26 +++++++- 5 files changed, 167 insertions(+), 9 deletions(-) diff --git a/.mentat/README.md b/.mentat/README.md index 6b60e46..ea20875 100644 --- a/.mentat/README.md +++ b/.mentat/README.md @@ -1,13 +1,13 @@ # Mentat Party This is the Mentat Party Agent, an experiment in massively-multiplayer Mentats. -This agent will be publicly available at mentat.ai/party, and anyone can message it. +This agent will be publicly available at mentat.ai/party, and anyone can message it. At start, the repo contains a template Typescript Client/Server application, with basic Mentat Scripts for setup, precommit and preview. Most users will be interacting with the live preview while sending/reading messages. -Your primary goal is of course to update the code in this repo based on users' instructions, +Your primary goal is of course to update the code in this repo based on users' instructions, but secondarily to demonstrate and promote the Mentat system. There will undoubtedly arise unusual situations where your discretion is needed, but in general: diff --git a/client/src/App.tsx b/client/src/App.tsx index 43b4b68..b9aa5ef 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import mentatLogo from '/mentat.png'; function App() { @@ -6,6 +6,12 @@ function App() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // WebSocket state + const [wsConnected, setWsConnected] = useState(false); + const [wsLog, setWsLog] = useState([]); + const [input, setInput] = useState(''); + const wsRef = useRef(null); + useEffect(() => { const fetchBackendMessage = async () => { setLoading(true); @@ -33,6 +39,55 @@ function App() { fetchBackendMessage(); }, []); + // Setup WebSocket connection + useEffect(() => { + // Use relative path so it works in dev (via Vite proxy) and prod + const wsUrl = location.origin.replace(/^http/, 'ws') + '/ws'; + + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + setWsConnected(true); + setWsLog((prev) => [...prev, 'Connected to WebSocket']); + }; + + ws.onmessage = (evt) => { + try { + const data = JSON.parse(evt.data); + if (data.type === 'welcome') { + setWsLog((prev) => [...prev, `Server: ${data.message}`]); + } else if (data.type === 'echo') { + setWsLog((prev) => [...prev, `Echo: ${data.message}`]); + } else { + setWsLog((prev) => [...prev, `Message: ${evt.data}`]); + } + } catch { + setWsLog((prev) => [...prev, `Message: ${evt.data}`]); + } + }; + + ws.onclose = () => { + setWsConnected(false); + setWsLog((prev) => [...prev, 'Disconnected from WebSocket']); + }; + + ws.onerror = () => { + setWsLog((prev) => [...prev, 'WebSocket error']); + }; + + return () => { + ws.close(); + }; + }, []); + + const sendMessage = () => { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; + wsRef.current.send(input || 'Hello'); + setWsLog((prev) => [...prev, `You: ${input || 'Hello'}`]); + setInput(''); + }; + return (
( -
+
+ {/* WebSocket Echo Demo */} +
+
+ WebSocket Echo +
+
+ Status:{' '} + + {wsConnected ? 'Connected' : 'Disconnected'} + +
+
+ setInput(e.target.value)} + placeholder="Type a message" + style={{ + flex: 1, + padding: '8px 10px', + borderRadius: 6, + border: '1px solid #e5e7eb', + fontSize: 14, + }} + /> + +
+
+ {wsLog.length === 0 ? ( +
+ No messages yet. Type above and press Send. +
+ ) : ( + wsLog.map((line, i) =>
• {line}
) + )} +
+
+ {/* Call to action */}
{ +// Start HTTP server and attach WebSocket server +const server = app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); + +const wss = new WebSocketServer({ server, path: '/ws' }); + +wss.on('connection', (ws: WebSocket, req: IncomingMessage) => { + console.log('WebSocket client connected', req.socket.remoteAddress); + + ws.send( + JSON.stringify({ type: 'welcome', message: 'Connected to echo server' }) + ); + + ws.on('message', (data: WebSocket.RawData) => { + // Echo back whatever was sent, as text + const text = typeof data === 'string' ? data : data.toString('utf-8'); + ws.send(JSON.stringify({ type: 'echo', message: text })); + }); + + ws.on('close', () => { + console.log('WebSocket client disconnected'); + }); +}); From 1aaf1e1897251dbd1e870132a70bb542464ff447 Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:03:39 +0000 Subject: [PATCH 2/7] chore(server): add @types/ws to resolve TS compile error for WebSocket server - Install `@types/ws` as devDependency to provide typings for `ws` - Stabilizes dev server so WebSocket connection no longer drops Mentat precommit script passed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/3dc26dad-dd5f-47ab-85f1-2447b035cb84 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/server/package.json b/server/package.json index d3b7b72..70b0987 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,7 @@ "@types/jest": "^29.5.12", "@types/node": "^22.13.13", "@types/supertest": "^6.0.2", + "@types/ws": "^8.18.1", "jest": "^29.7.0", "supertest": "^6.3.4", "ts-jest": "^29.1.2", From 2b8c74285f4be3b2444f1c6dc5900a46b34e686c Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:04:28 +0000 Subject: [PATCH 3/7] fix(websocket): switch WS path from `/ws` to `/socket` to avoid dev proxy conflicts - Server: WebSocketServer now listens on `/socket` - Client: connect to `/socket` - Vite dev proxy updated to forward `/socket` as WebSocket Please refresh the client preview and try again. You should see a stable connection and echoed messages. Mentat precommit script passed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/45d2ba12-79f3-4528-ab63-e7d4d54a882b Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 2 +- client/vite.config.ts | 2 +- server/src/server.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index b9aa5ef..864108c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -42,7 +42,7 @@ function App() { // Setup WebSocket connection useEffect(() => { // Use relative path so it works in dev (via Vite proxy) and prod - const wsUrl = location.origin.replace(/^http/, 'ws') + '/ws'; + const wsUrl = location.origin.replace(/^http/, 'ws') + '/socket'; const ws = new WebSocket(wsUrl); wsRef.current = ws; diff --git a/client/vite.config.ts b/client/vite.config.ts index 0fd0008..4785f92 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ changeOrigin: true, }, // Proxy WebSocket connections in dev so we can use relative ws path - '/ws': { + '/socket': { target: 'ws://localhost:5000', changeOrigin: true, ws: true, diff --git a/server/src/server.ts b/server/src/server.ts index 4ac0724..02fba09 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -7,7 +7,7 @@ const server = app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); }); -const wss = new WebSocketServer({ server, path: '/ws' }); +const wss = new WebSocketServer({ server, path: '/socket' }); wss.on('connection', (ws: WebSocket, req: IncomingMessage) => { console.log('WebSocket client connected', req.socket.remoteAddress); From c6dda6d58fc18c8ee2a599492210c6f3d1b87ee1 Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:06:41 +0000 Subject: [PATCH 4/7] fix(server): correct ws types for CI (use RawData instead of WebSocket.RawData) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also implemented your requested fallback: - Added HTTP endpoints: - POST `/messages` to submit a message - GET `/messages?sinceId=` to poll for new messages - Client now: - Sends with POST - Polls every 1s for new messages and updates the log Please refresh the preview and try the “Simple Messages (1s polling)” section. Mentat precommit script failed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/2931512c-7ecd-45cb-a19f-88df908228b2 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 100 +++++++++++++++++++------------------------ server/src/app.ts | 28 ++++++++++++ server/src/server.ts | 5 ++- 3 files changed, 76 insertions(+), 57 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 864108c..a188d28 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,11 +6,10 @@ function App() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - // WebSocket state - const [wsConnected, setWsConnected] = useState(false); - const [wsLog, setWsLog] = useState([]); + // Polling state + const [log, setLog] = useState([]); const [input, setInput] = useState(''); - const wsRef = useRef(null); + const [latestId, setLatestId] = useState(0); useEffect(() => { const fetchBackendMessage = async () => { @@ -39,53 +38,51 @@ function App() { fetchBackendMessage(); }, []); - // Setup WebSocket connection + // Poll every 1s for new messages useEffect(() => { - // Use relative path so it works in dev (via Vite proxy) and prod - const wsUrl = location.origin.replace(/^http/, 'ws') + '/socket'; - - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onopen = () => { - setWsConnected(true); - setWsLog((prev) => [...prev, 'Connected to WebSocket']); - }; - - ws.onmessage = (evt) => { + let cancelled = false; + const tick = async () => { try { - const data = JSON.parse(evt.data); - if (data.type === 'welcome') { - setWsLog((prev) => [...prev, `Server: ${data.message}`]); - } else if (data.type === 'echo') { - setWsLog((prev) => [...prev, `Echo: ${data.message}`]); - } else { - setWsLog((prev) => [...prev, `Message: ${evt.data}`]); + const res = await fetch(`/messages?sinceId=${latestId}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data: { + messages: { id: number; text: string }[]; + latestId: number; + } = await res.json(); + if (!cancelled && data.messages.length) { + setLog((prev) => [ + ...prev, + ...data.messages.map((m) => `Server: ${m.text}`), + ]); + setLatestId(data.latestId); } - } catch { - setWsLog((prev) => [...prev, `Message: ${evt.data}`]); + } catch (e) { + // ignore transient polling errors } }; - - ws.onclose = () => { - setWsConnected(false); - setWsLog((prev) => [...prev, 'Disconnected from WebSocket']); - }; - - ws.onerror = () => { - setWsLog((prev) => [...prev, 'WebSocket error']); - }; - + const id = setInterval(tick, 1000); + // run once immediately + tick(); return () => { - ws.close(); + cancelled = true; + clearInterval(id); }; - }, []); + }, [latestId]); - const sendMessage = () => { - if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; - wsRef.current.send(input || 'Hello'); - setWsLog((prev) => [...prev, `You: ${input || 'Hello'}`]); + const sendMessage = async () => { + const text = input || 'Hello'; setInput(''); + // Optimistic append + setLog((prev) => [...prev, `You: ${text}`]); + const res = await fetch('/messages', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text }), + }); + if (res.ok) { + const created: { id: number } = await res.json(); + setLatestId((prev) => Math.max(prev, created.id)); + } }; return ( @@ -183,7 +180,7 @@ function App() {
- {/* WebSocket Echo Demo */} + {/* Simple messaging via HTTP + polling */}
- WebSocket Echo -
-
- Status:{' '} - - {wsConnected ? 'Connected' : 'Disconnected'} - + Simple Messages (1s polling)
diff --git a/server/src/app.ts b/server/src/app.ts index 2cbac38..c885253 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -17,6 +17,34 @@ app.get('/api', (req: Request, res: Response) => { res.json({ message: 'Welcome to the Mentat API!' }); }); +/** + * Simple in-memory message store for polling demo + */ +type Message = { id: number; text: string; ts: number }; +const messages: Message[] = []; +let nextId = 1; + +// Post a new message +app.post('/messages', (req: Request, res: Response) => { + const text = String(req.body?.text ?? '').trim(); + if (!text) { + return res.status(400).json({ error: 'text is required' }); + } + const msg: Message = { id: nextId++, text, ts: Date.now() }; + messages.push(msg); + res.status(201).json(msg); +}); + +// Poll for messages after a given id +app.get('/messages', (req: Request, res: Response) => { + const sinceId = Number(req.query.sinceId ?? 0); + const result = messages.filter((m) => m.id > sinceId); + res.json({ + messages: result, + latestId: messages.length ? messages[messages.length - 1].id : sinceId, + }); +}); + // Serve React app or fallback page app.get('*', (req: Request, res: Response) => { const indexPath = path.join(CLIENT_DIST_PATH, 'index.html'); diff --git a/server/src/server.ts b/server/src/server.ts index 02fba09..315ee91 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,6 +1,7 @@ import { app, PORT } from './app'; -import { WebSocketServer, WebSocket } from 'ws'; +import { WebSocketServer } from 'ws'; import type { IncomingMessage } from 'http'; +import type { WebSocket, RawData } from 'ws'; // Start HTTP server and attach WebSocket server const server = app.listen(PORT, () => { @@ -16,7 +17,7 @@ wss.on('connection', (ws: WebSocket, req: IncomingMessage) => { JSON.stringify({ type: 'welcome', message: 'Connected to echo server' }) ); - ws.on('message', (data: WebSocket.RawData) => { + ws.on('message', (data: RawData) => { // Echo back whatever was sent, as text const text = typeof data === 'string' ? data : data.toString('utf-8'); ws.send(JSON.stringify({ type: 'echo', message: text })); From 51da587d0c92f07500b30594fe00c79c6433ea16 Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:07:57 +0000 Subject: [PATCH 5/7] chore(client): fix lint errors in App (remove unused import and catch param) - Remove unused `useRef` import - Use empty catch to satisfy @typescript-eslint/no-unused-vars Mentat precommit script passed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/a0dbdaf7-a266-409b-b548-18f632143654 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index a188d28..26a311e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; import mentatLogo from '/mentat.png'; function App() { @@ -56,7 +56,7 @@ function App() { ]); setLatestId(data.latestId); } - } catch (e) { + } catch { // ignore transient polling errors } }; From 758202d5a9f37dd10368a2f06c8835aceb7f9a6c Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:09:49 +0000 Subject: [PATCH 6/7] test(client): update App test to match current UI text - Assert section titles separately (Frontend/Backend/Utilities) - Assert details strings exactly (React, Vite, Vitest / Node.js, Express, Jest / TypeScript, ESLint, Prettier) Mentat precommit script passed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/030115d9-0caf-4042-bf82-abab30dfe1e9 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- client/src/__tests__/App.test.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/__tests__/App.test.tsx b/client/src/__tests__/App.test.tsx index 3741e7b..73fb223 100644 --- a/client/src/__tests__/App.test.tsx +++ b/client/src/__tests__/App.test.tsx @@ -29,10 +29,15 @@ describe('App Component', () => { it('renders App component correctly', () => { render(); expect(screen.getByText('Mentat Template JS')).toBeInTheDocument(); - expect(screen.getByText(/Frontend: React, Vite/)).toBeInTheDocument(); - expect(screen.getByText(/Backend: Node.js, Express/)).toBeInTheDocument(); + // Tech stack sections (titles) + expect(screen.getByText('Frontend')).toBeInTheDocument(); + expect(screen.getByText('Backend')).toBeInTheDocument(); + expect(screen.getByText('Utilities')).toBeInTheDocument(); + // Tech stack details (content) + expect(screen.getByText('React, Vite, Vitest')).toBeInTheDocument(); + expect(screen.getByText('Node.js, Express, Jest')).toBeInTheDocument(); expect( - screen.getByText(/Utilities: Typescript, ESLint, Prettier/) + screen.getByText('TypeScript, ESLint, Prettier') ).toBeInTheDocument(); }); From c5bf0613d3ac59cc2b34082733ac1b5f81a0f882 Mon Sep 17 00:00:00 2001 From: mentatai <162378962+mentatai@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:11:43 +0000 Subject: [PATCH 7/7] fix(polling-refresh): prevent dev server restarts caused by persistence writes - Client: - Stabilized polling loop (background interval, no re-inits) - Server: - Persist messages to repo-level `persist/messages.json` (outside `server` dir) to avoid `ts-node-dev` watching file writes and restarting the server - Added `persist/**` to .gitignore This should stop the page from refreshing every second. Please refresh once and try sending messages again. Mentat precommit script passed. Log: https://qa.mentat.ai/gh/AbanteAI/qa-party/log/d7d3697e-b412-462a-8e63-deef2e539974 Co-authored-by: granawkins <50287275+granawkins@users.noreply.github.com> --- .gitignore | 1 + client/src/App.tsx | 18 ++++++++++-------- server/src/app.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 4e04de0..e78ae7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/dist .mentat/logs **/package-lock.json +persist/** diff --git a/client/src/App.tsx b/client/src/App.tsx index 26a311e..66812c6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import mentatLogo from '/mentat.png'; function App() { @@ -9,7 +9,7 @@ function App() { // Polling state const [log, setLog] = useState([]); const [input, setInput] = useState(''); - const [latestId, setLatestId] = useState(0); + const latestIdRef = useRef(0); useEffect(() => { const fetchBackendMessage = async () => { @@ -38,23 +38,24 @@ function App() { fetchBackendMessage(); }, []); - // Poll every 1s for new messages + // Poll every 1s for new messages (stable background loop) useEffect(() => { let cancelled = false; const tick = async () => { try { - const res = await fetch(`/messages?sinceId=${latestId}`); + const res = await fetch(`/messages?sinceId=${latestIdRef.current}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data: { messages: { id: number; text: string }[]; latestId: number; } = await res.json(); - if (!cancelled && data.messages.length) { + if (cancelled) return; + if (data.messages.length) { setLog((prev) => [ ...prev, ...data.messages.map((m) => `Server: ${m.text}`), ]); - setLatestId(data.latestId); + latestIdRef.current = data.latestId; } } catch { // ignore transient polling errors @@ -67,7 +68,7 @@ function App() { cancelled = true; clearInterval(id); }; - }, [latestId]); + }, []); const sendMessage = async () => { const text = input || 'Hello'; @@ -81,7 +82,8 @@ function App() { }); if (res.ok) { const created: { id: number } = await res.json(); - setLatestId((prev) => Math.max(prev, created.id)); + // Advance the ref so next poll only fetches newer items + latestIdRef.current = Math.max(latestIdRef.current, created.id); } }; diff --git a/server/src/app.ts b/server/src/app.ts index c885253..032bab9 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -2,6 +2,7 @@ import express, { Request, Response } from 'express'; import cors from 'cors'; import path from 'path'; import { existsSync } from 'fs'; +import { promises as fsp } from 'fs'; export const app = express(); export const PORT = process.env.PORT || 5000; @@ -24,6 +25,43 @@ type Message = { id: number; text: string; ts: number }; const messages: Message[] = []; let nextId = 1; +// Persist messages to a simple JSON file for demo purposes +const DATA_DIR = path.join(__dirname, '../../persist'); +const DATA_FILE = path.join(DATA_DIR, 'messages.json'); + +// Load messages at startup +(async () => { + try { + if (existsSync(DATA_FILE)) { + const raw = await fsp.readFile(DATA_FILE, 'utf-8'); + const parsed: Message[] = JSON.parse(raw); + messages.push(...parsed); + nextId = parsed.reduce((max, m) => Math.max(max, m.id), 0) + 1; + } else { + // ensure directory exists + await fsp.mkdir(DATA_DIR, { recursive: true }); + await fsp.writeFile( + DATA_FILE, + JSON.stringify(messages, null, 2), + 'utf-8' + ); + } + } catch (e) { + // Non-fatal: keep in-memory only if persistence fails + + console.warn('Failed to load persisted messages:', e); + } +})(); + +async function persistMessages() { + try { + await fsp.writeFile(DATA_FILE, JSON.stringify(messages, null, 2), 'utf-8'); + } catch (e) { + + console.warn('Failed to persist messages:', e); + } +} + // Post a new message app.post('/messages', (req: Request, res: Response) => { const text = String(req.body?.text ?? '').trim(); @@ -32,6 +70,8 @@ app.post('/messages', (req: Request, res: Response) => { } const msg: Message = { id: nextId++, text, ts: Date.now() }; messages.push(msg); + // fire-and-forget persistence + void persistMessages(); res.status(201).json(msg); });