@yeaft/webchat-agent 0.1.859 → 0.1.863

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.
@@ -45,6 +45,15 @@ import {
45
45
  groupsRoot,
46
46
  } from './groups/group-crud.js';
47
47
  import { openGroup, loadGroupMeta } from './groups/group-store.js';
48
+ import {
49
+ createChat as createChatStore,
50
+ openChat,
51
+ listChats,
52
+ renameChat as renameChatStore,
53
+ archiveChat as archiveChatStore,
54
+ deleteChat as deleteChatStore,
55
+ touchChat,
56
+ } from './chats/chat-store.js';
48
57
  import { loadGroupConfig, resolveGroupConfig, GroupConfigError } from './groups/group-config.js';
49
58
  import { updateGroupConfig } from './groups/group-crud.js';
50
59
  import { createCoordinator } from './groups/coordinator.js';
@@ -1254,12 +1263,13 @@ function resolveGroupDefaultVpId(groupId) {
1254
1263
  }
1255
1264
  }
1256
1265
 
1257
- function sendYeaftOutput(data, { groupId, vpId, turnId, threadId } = {}) {
1258
- const resolvedVpId = vpId || resolveGroupDefaultVpId(groupId);
1266
+ function sendYeaftOutput(data, { groupId, chatId, vpId, turnId, threadId } = {}) {
1267
+ const resolvedVpId = vpId || (groupId ? resolveGroupDefaultVpId(groupId) : null);
1259
1268
  sendToServer({
1260
1269
  type: 'yeaft_output',
1261
1270
  conversationId: yeaftConversationId,
1262
1271
  ...(groupId ? { groupId } : {}),
1272
+ ...(chatId ? { chatId } : {}),
1263
1273
  ...(resolvedVpId ? { vpId: resolvedVpId } : {}),
1264
1274
  ...(turnId ? { turnId } : {}),
1265
1275
  ...(threadId ? { threadId } : {}),
@@ -1268,11 +1278,12 @@ function sendYeaftOutput(data, { groupId, vpId, turnId, threadId } = {}) {
1268
1278
  }
1269
1279
 
1270
1280
  /** Send a yeaft_output event (non-claude_output metadata). */
1271
- function sendYeaftEvent(event, { groupId, vpId, turnId, threadId } = {}) {
1281
+ function sendYeaftEvent(event, { groupId, chatId, vpId, turnId, threadId } = {}) {
1272
1282
  sendToServer({
1273
1283
  type: 'yeaft_output',
1274
1284
  conversationId: yeaftConversationId,
1275
1285
  ...(groupId ? { groupId } : {}),
1286
+ ...(chatId ? { chatId } : {}),
1276
1287
  ...(vpId ? { vpId } : {}),
1277
1288
  ...(turnId ? { turnId } : {}),
1278
1289
  ...(threadId ? { threadId } : {}),
@@ -1665,6 +1676,318 @@ export function handleYeaftSetDefaultVp(msg) {
1665
1676
  }
1666
1677
  }
1667
1678
 
1679
+ // ─── Yeaft Chat Mode ──────────────────────────────────────────
1680
+ //
1681
+ // 1:1 conversation between the user and a single VP. Reuses the engine,
1682
+ // memory, tool, and adapter stacks; bypasses the group coordinator and
1683
+ // fan-out machinery entirely. Persisted under `~/.yeaft/chats/<chatId>/`.
1684
+
1685
+ const chatEngines = new Map(); // key: `${chatId}::${vpId}` → Engine
1686
+ const chatAborts = new Map(); // key: chatId → AbortController for the in-flight turn
1687
+
1688
+ function chatsRootFor(yeaftDir) {
1689
+ return join(yeaftDir, 'chats');
1690
+ }
1691
+
1692
+ function snapshotChats(yeaftDir) {
1693
+ if (!yeaftDir) return [];
1694
+ try { return listChats(chatsRootFor(yeaftDir)); }
1695
+ catch { return []; }
1696
+ }
1697
+
1698
+ function sendChatCrudResult(payload) {
1699
+ sendYeaftEvent({ type: 'chat_crud_result', ...payload });
1700
+ }
1701
+
1702
+ function sendChatSnapshotBroadcast() {
1703
+ try {
1704
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1705
+ if (!yeaftDir) return;
1706
+ sendYeaftEvent({ type: 'chat_list_updated', chats: snapshotChats(yeaftDir) });
1707
+ } catch (err) {
1708
+ console.warn('[Yeaft] sendChatSnapshotBroadcast failed:', err?.message || err);
1709
+ }
1710
+ }
1711
+
1712
+ function chatErrorPayload(err) {
1713
+ return {
1714
+ code: (err && err.code) || 'unknown',
1715
+ chatId: err && err.chatId,
1716
+ message: err && err.message,
1717
+ };
1718
+ }
1719
+
1720
+ export function handleYeaftListChats(msg) {
1721
+ const requestId = msg && msg.requestId;
1722
+ try {
1723
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1724
+ sendChatCrudResult({ op: 'list', requestId, ok: true, chats: snapshotChats(yeaftDir) });
1725
+ } catch (err) {
1726
+ sendChatCrudResult({ op: 'list', requestId, ok: false, error: chatErrorPayload(err) });
1727
+ }
1728
+ }
1729
+
1730
+ export function handleYeaftCreateChat(msg) {
1731
+ const requestId = msg && msg.requestId;
1732
+ const payload = (msg && msg.payload) || {};
1733
+ try {
1734
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1735
+ if (!yeaftDir) throw new Error('no yeaft directory configured');
1736
+ if (!payload.vpId) throw new Error('vpId required');
1737
+ const root = chatsRootFor(yeaftDir);
1738
+ const chatId = (payload.id && String(payload.id).trim())
1739
+ || `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1740
+ const h = createChatStore(root, {
1741
+ id: chatId,
1742
+ vpId: payload.vpId,
1743
+ displayName: payload.displayName,
1744
+ workDir: payload.workDir,
1745
+ });
1746
+ const meta = h.getMeta();
1747
+ h.close();
1748
+ sendChatCrudResult({ op: 'create', requestId, ok: true, chat: meta });
1749
+ sendChatSnapshotBroadcast();
1750
+ } catch (err) {
1751
+ sendChatCrudResult({ op: 'create', requestId, ok: false, error: chatErrorPayload(err) });
1752
+ }
1753
+ }
1754
+
1755
+ export function handleYeaftRenameChat(msg) {
1756
+ const requestId = msg && msg.requestId;
1757
+ const chatId = msg && msg.chatId;
1758
+ const displayName = msg && msg.displayName;
1759
+ try {
1760
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1761
+ const chat = renameChatStore(chatsRootFor(yeaftDir), chatId, displayName);
1762
+ sendChatCrudResult({ op: 'rename', requestId, ok: true, chat });
1763
+ sendChatSnapshotBroadcast();
1764
+ } catch (err) {
1765
+ sendChatCrudResult({ op: 'rename', requestId, ok: false, error: chatErrorPayload(err) });
1766
+ }
1767
+ }
1768
+
1769
+ export function handleYeaftArchiveChat(msg) {
1770
+ const requestId = msg && msg.requestId;
1771
+ const chatId = msg && msg.chatId;
1772
+ try {
1773
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1774
+ const ok = archiveChatStore(chatsRootFor(yeaftDir), chatId);
1775
+ if (!ok) throw new Error(`chat ${chatId} not found`);
1776
+ invalidateChatRuntime(chatId);
1777
+ sendChatCrudResult({ op: 'archive', requestId, ok: true, chatId });
1778
+ sendChatSnapshotBroadcast();
1779
+ } catch (err) {
1780
+ sendChatCrudResult({ op: 'archive', requestId, ok: false, error: chatErrorPayload(err) });
1781
+ }
1782
+ }
1783
+
1784
+ export function handleYeaftDeleteChat(msg) {
1785
+ const requestId = msg && msg.requestId;
1786
+ const chatId = msg && msg.chatId;
1787
+ try {
1788
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1789
+ const ok = deleteChatStore(chatsRootFor(yeaftDir), chatId);
1790
+ if (!ok) throw new Error(`chat ${chatId} not found`);
1791
+ invalidateChatRuntime(chatId);
1792
+ sendChatCrudResult({ op: 'delete', requestId, ok: true, chatId });
1793
+ sendChatSnapshotBroadcast();
1794
+ } catch (err) {
1795
+ sendChatCrudResult({ op: 'delete', requestId, ok: false, error: chatErrorPayload(err) });
1796
+ }
1797
+ }
1798
+
1799
+ function invalidateChatRuntime(chatId) {
1800
+ if (!chatId) return;
1801
+ const prefix = `${chatId}::`;
1802
+ for (const k of Array.from(chatEngines.keys())) {
1803
+ if (k.startsWith(prefix)) chatEngines.delete(k);
1804
+ }
1805
+ const ctrl = chatAborts.get(chatId);
1806
+ if (ctrl) {
1807
+ try { ctrl.abort(); } catch { /* best-effort */ }
1808
+ chatAborts.delete(chatId);
1809
+ }
1810
+ }
1811
+
1812
+ function getOrCreateChatEngine(chatId, vpId) {
1813
+ const key = `${chatId}::${vpId}`;
1814
+ let eng = chatEngines.get(key);
1815
+ if (eng) return eng;
1816
+ if (!session) throw new Error('getOrCreateChatEngine: session not loaded');
1817
+ eng = new Engine({
1818
+ adapter: session.adapter,
1819
+ trace: session.trace,
1820
+ config: session.config,
1821
+ conversationStore: session.conversationStore,
1822
+ memoryIndex: session.memoryIndex || null,
1823
+ amsRegistry: session.amsRegistry,
1824
+ toolRegistry: session.toolRegistry,
1825
+ skillManager: session.skillManager,
1826
+ mcpManager: session.mcpManager,
1827
+ yeaftDir: session.yeaftDir,
1828
+ toolStats: session.toolStats || null,
1829
+ chatId,
1830
+ vpId,
1831
+ });
1832
+ chatEngines.set(key, eng);
1833
+ return eng;
1834
+ }
1835
+
1836
+ /**
1837
+ * Send a single user turn in a chat session. 1:1 — no fan-out, no
1838
+ * coordinator, no @-mention dispatch. Engine events are forwarded as
1839
+ * yeaft_output with `chatId` instead of `groupId`.
1840
+ */
1841
+ export async function handleYeaftChatSend(msg) {
1842
+ if (!msg || typeof msg !== 'object') return;
1843
+ const chatId = typeof msg.chatId === 'string' ? msg.chatId.trim() : '';
1844
+ const text = typeof msg.text === 'string' ? msg.text : '';
1845
+ if (!chatId) return;
1846
+ if (!text.trim()) return;
1847
+
1848
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1849
+ if (!yeaftDir) {
1850
+ sendYeaftOutput({
1851
+ type: 'assistant',
1852
+ message: { content: [{ type: 'text', text: '⚠️ Yeaft session error: no yeaft directory configured.' }] },
1853
+ }, { chatId });
1854
+ sendYeaftOutput({ type: 'result', result_text: '' }, { chatId });
1855
+ return;
1856
+ }
1857
+
1858
+ await ensureSessionLoaded();
1859
+
1860
+ const root = chatsRootFor(yeaftDir);
1861
+ let handle;
1862
+ try { handle = openChat(root, chatId); }
1863
+ catch (err) {
1864
+ sendYeaftOutput({
1865
+ type: 'assistant',
1866
+ message: { content: [{ type: 'text', text: `⚠️ ${err.message}` }] },
1867
+ }, { chatId });
1868
+ sendYeaftOutput({ type: 'result', result_text: '' }, { chatId });
1869
+ return;
1870
+ }
1871
+ const meta = handle.getMeta();
1872
+ if (!meta) {
1873
+ handle.close();
1874
+ sendYeaftOutput({
1875
+ type: 'assistant',
1876
+ message: { content: [{ type: 'text', text: `⚠️ Chat ${chatId} not found.` }] },
1877
+ }, { chatId });
1878
+ sendYeaftOutput({ type: 'result', result_text: '' }, { chatId });
1879
+ return;
1880
+ }
1881
+ const vpId = meta.vpId;
1882
+
1883
+ // Persist attachments + build LLM-side multimodal parts. Same flow as
1884
+ // the group path (handleYeaftGroupChat) so chat mode isn't a
1885
+ // second-class citizen for images / files.
1886
+ const inboundFiles = Array.isArray(msg.files) ? msg.files : [];
1887
+ let attachmentBundle = { promptAttachments: [], promptSuffix: '', promptParts: [], failed: [] };
1888
+ if (inboundFiles.length > 0) {
1889
+ try {
1890
+ attachmentBundle = persistYeaftAttachments(inboundFiles, { subdir: `chat-${chatId}` });
1891
+ } catch (err) {
1892
+ console.warn('[Yeaft] yeaft_chat_send: attachment persist failed', err?.message || err);
1893
+ }
1894
+ }
1895
+ if (Array.isArray(attachmentBundle.failed) && attachmentBundle.failed.length > 0) {
1896
+ const detail = attachmentBundle.failed.map((f) => ` - ${f.name}: ${f.error}`).join('\n');
1897
+ sendYeaftOutput({
1898
+ type: 'assistant',
1899
+ message: { content: [{ type: 'text', text: `⚠️ ${attachmentBundle.failed.length} file(s) could not be attached:\n${detail}` }] },
1900
+ }, { chatId });
1901
+ }
1902
+ const persistedAttachments = attachmentsForPersistence(attachmentBundle.promptAttachments);
1903
+
1904
+ // Append the user message to the chat log so subsequent reads see it.
1905
+ try {
1906
+ handle.appendMessage({ from: 'user', role: 'user', text, meta: { attachments: persistedAttachments } });
1907
+ } catch (err) {
1908
+ console.warn('[Yeaft] chat appendMessage failed:', err?.message || err);
1909
+ } finally {
1910
+ handle.close();
1911
+ }
1912
+
1913
+ const turnId = `t_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1914
+ const envelope = { chatId, vpId, turnId };
1915
+
1916
+ // Cancel any in-flight turn on this chat — 1:1 has no parallelism.
1917
+ const prior = chatAborts.get(chatId);
1918
+ if (prior) { try { prior.abort(); } catch { /* ignore */ } }
1919
+ const abort = new AbortController();
1920
+ chatAborts.set(chatId, abort);
1921
+
1922
+ sendYeaftEvent({ type: 'vp_turn_start', vpId, threadId: 'main', turnId, chatId, title: meta.displayName || '' }, envelope);
1923
+
1924
+ let queryTimer = null;
1925
+ const resetQueryTimer = () => {
1926
+ if (queryTimer) clearTimeout(queryTimer);
1927
+ queryTimer = setTimeout(() => {
1928
+ if (!abort.signal.aborted) {
1929
+ try { abort.abort(); } catch { /* ignore */ }
1930
+ }
1931
+ }, QUERY_TIMEOUT_MS);
1932
+ };
1933
+ resetQueryTimer();
1934
+
1935
+ try {
1936
+ const assistantTextParts = [];
1937
+ const toolCallsAccum = [];
1938
+ const toolResultsAccum = [];
1939
+ const thinkingBlocksAccum = [];
1940
+ const appendedUserPrompts = [];
1941
+
1942
+ const eng = getOrCreateChatEngine(chatId, vpId);
1943
+ const handlerCtx = {
1944
+ assistantTextParts,
1945
+ toolCallsAccum,
1946
+ toolResultsAccum,
1947
+ thinkingBlocksAccum,
1948
+ resetQueryTimer,
1949
+ chatId,
1950
+ vpId,
1951
+ turnId,
1952
+ threadId: 'main',
1953
+ thread: null,
1954
+ appendedUserPrompts,
1955
+ };
1956
+
1957
+ for await (const event of eng.query({
1958
+ prompt: text,
1959
+ promptParts: attachmentBundle.promptParts && attachmentBundle.promptParts.length > 0 ? attachmentBundle.promptParts : null,
1960
+ signal: abort.signal,
1961
+ userAlreadyPersisted: false,
1962
+ threadId: 'main',
1963
+ senderVpId: vpId,
1964
+ })) {
1965
+ resetQueryTimer();
1966
+ handleEngineEvent(event, handlerCtx);
1967
+ }
1968
+
1969
+ sendYeaftOutput({ type: 'assistant', message: { content: [] } }, envelope);
1970
+ sendYeaftOutput({ type: 'result', result_text: '' }, envelope);
1971
+
1972
+ try { touchChat(root, chatId); } catch { /* best-effort */ }
1973
+ } catch (err) {
1974
+ const isAbort = err && (err.name === 'AbortError' || err.name === 'LLMAbortError');
1975
+ if (isAbort) {
1976
+ sendYeaftOutput({ type: 'result', result_text: '', stopped: true }, envelope);
1977
+ } else {
1978
+ console.error('[Yeaft] chat-send error:', err);
1979
+ sendYeaftOutput({
1980
+ type: 'assistant',
1981
+ message: { content: [{ type: 'text', text: `⚠️ Chat error: ${err.message}` }] },
1982
+ }, envelope);
1983
+ sendYeaftOutput({ type: 'result', result_text: '' }, envelope);
1984
+ }
1985
+ } finally {
1986
+ if (queryTimer) clearTimeout(queryTimer);
1987
+ if (chatAborts.get(chatId) === abort) chatAborts.delete(chatId);
1988
+ }
1989
+ }
1990
+
1668
1991
  /**
1669
1992
  * Install the dream pipeline progress sink and runtime settings bridge.
1670
1993
  * Thread scheduling is owned by the group VP runtime below, not by mutable