@yeaft/webchat-agent 0.1.910 → 0.1.912

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.
@@ -37,7 +37,7 @@ import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
37
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
39
39
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
40
- import { handleYeaftSessionSend, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
40
+ import { handleYeaftSessionSend, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
41
41
 
42
42
  export async function handleMessage(msg) {
43
43
  switch (msg.type) {
@@ -578,6 +578,16 @@ export async function handleMessage(msg) {
578
578
  case 'yeaft_session_set_default_vp':
579
579
  handleYeaftSessionSetDefaultVp(msg);
580
580
  break;
581
+ // feat-yeaft-session-restore: probe + register a session by workdir.
582
+ // `scan_workdir` is read-only (lists what's on disk + flags whether it's
583
+ // already in the central registry); `restore` writes the registry entry
584
+ // and triggers a snapshot rebroadcast so the sidebar updates.
585
+ case 'yeaft_scan_workdir_sessions':
586
+ handleYeaftScanWorkdirSessions(msg);
587
+ break;
588
+ case 'yeaft_restore_session':
589
+ handleYeaftRestoreSession(msg);
590
+ break;
581
591
  // Phase 2: session_send is just group_chat (N≥1 fan-out already works).
582
592
  case 'yeaft_session_send':
583
593
  handleYeaftSessionSend(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.910",
3
+ "version": "0.1.912",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -52,6 +52,8 @@ export {
52
52
  snapshotSessions,
53
53
  updateSessionConfig,
54
54
  updateSessionAnnouncement,
55
+ scanWorkdirSessions,
56
+ restoreSessionToRegistry,
55
57
  } from './session-crud.js';
56
58
  export {
57
59
  loadSessionConfig,
@@ -152,6 +152,86 @@ export function unregisterSessionWorkDir(defaultYeaftDir, sessionId) {
152
152
  writeWorkDirRegistry(defaultYeaftDir, registry);
153
153
  }
154
154
 
155
+ /**
156
+ * Scan the `.yeaft/sessions/` directory under `workDir` and return every
157
+ * session meta we can read. Read-only: never touches the registry.
158
+ *
159
+ * Each returned record carries `workDir` (the normalized path we scanned).
160
+ * This utility deliberately does NOT decorate `alreadyRegistered` — that
161
+ * cross-references the central workdir registry, which is a separate
162
+ * concern owned by the handler / caller (see
163
+ * `handleYeaftScanWorkdirSessions`). Keeping the utility layer-pure makes
164
+ * it usable from contexts that don't have / don't care about the registry
165
+ * (e.g. CLI tools, future per-workdir snapshots).
166
+ *
167
+ * Returns `[]` for missing dir / empty dir / unreadable dir — never throws.
168
+ * Sort is `createdAt` descending (most-recent first) because the restore
169
+ * UI typically cares about "the session I made yesterday".
170
+ *
171
+ * @param {string} workDir — the working directory to scan.
172
+ * @returns {Array<object>}
173
+ */
174
+ export function scanWorkdirSessions(workDir) {
175
+ const normalized = normalizeWorkDir(workDir);
176
+ if (!normalized) return [];
177
+ const groupYeaftDir = yeaftDirForWorkDir(normalized);
178
+ const root = sessionsRoot(groupYeaftDir);
179
+ if (!existsSync(root)) return [];
180
+ const out = [];
181
+ let entries;
182
+ try {
183
+ entries = readdirSync(root);
184
+ } catch {
185
+ return [];
186
+ }
187
+ for (const name of entries) {
188
+ if (name.startsWith('.')) continue; // skip .archived-* and dotfiles
189
+ const dir = join(root, name);
190
+ try { if (!statSync(dir).isDirectory()) continue; } catch { continue; }
191
+ const meta = loadSessionMeta(dir);
192
+ if (!meta) continue;
193
+ out.push({
194
+ ...meta,
195
+ workDir: normalized,
196
+ });
197
+ }
198
+ return out.sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')));
199
+ }
200
+
201
+ /**
202
+ * Register `(sessionId, workDir)` in the central registry so the next
203
+ * `snapshotSessions()` includes this session.
204
+ *
205
+ * Validates that `<workDir>/.yeaft/sessions/<sessionId>/group.json` exists
206
+ * and is parseable. Throws:
207
+ * - `not_found` — the session dir is not on disk at this workdir
208
+ * - `corrupt_meta` — the dir exists but `group.json` is missing / unreadable
209
+ * / can't be parsed. Surfaced as a distinct code so the
210
+ * UI can tell the user "the file is broken" instead of
211
+ * "you picked the wrong workdir" (review finding I1).
212
+ *
213
+ * Idempotent: if the same `(sessionId, workDir)` is already registered,
214
+ * we still rewrite the entry (with the normalized path) and return the
215
+ * fresh meta — no error.
216
+ *
217
+ * @param {string} defaultYeaftDir
218
+ * @param {string} sessionId
219
+ * @param {string} workDir
220
+ * @returns {object} the session meta, with `workDir` set to the normalized path.
221
+ */
222
+ export function restoreSessionToRegistry(defaultYeaftDir, sessionId, workDir) {
223
+ if (!sessionId) throw new SessionCrudError('invalid_session_id', null);
224
+ const normalized = normalizeWorkDir(workDir);
225
+ if (!normalized) throw new SessionCrudError('invalid_workdir', sessionId);
226
+ const groupYeaftDir = yeaftDirForWorkDir(normalized);
227
+ const dir = join(sessionsRoot(groupYeaftDir), sessionId);
228
+ if (!existsSync(dir)) throw new SessionCrudError('not_found', sessionId);
229
+ const meta = loadSessionMeta(dir);
230
+ if (!meta) throw new SessionCrudError('corrupt_meta', sessionId, `group.json missing or unreadable at ${dir}`);
231
+ registerSessionWorkDir(defaultYeaftDir, sessionId, normalized);
232
+ return { ...meta, workDir: normalized };
233
+ }
234
+
155
235
  export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
156
236
  if (!defaultYeaftDir || !sessionId) return defaultYeaftDir;
157
237
  const defaultGroupDir = join(sessionsRoot(defaultYeaftDir), sessionId);
@@ -43,6 +43,9 @@ import {
43
43
  snapshotSessions,
44
44
  resolveSessionYeaftDir,
45
45
  sessionsRoot,
46
+ scanWorkdirSessions,
47
+ restoreSessionToRegistry,
48
+ readWorkDirRegistry,
46
49
  } from './sessions/session-crud.js';
47
50
  import { openSession, loadSessionMeta } from './sessions/session-store.js';
48
51
  import { loadSessionConfig, resolveSessionConfig, SessionConfigError } from './sessions/session-config.js';
@@ -1501,6 +1504,63 @@ export function handleYeaftCreateSession(msg) {
1501
1504
  }
1502
1505
  }
1503
1506
 
1507
+ /**
1508
+ * `yeaft_scan_workdir_sessions` — read-only probe: list every yeaft
1509
+ * session physically present under `<workDir>/.yeaft/sessions/` along with
1510
+ * an `alreadyRegistered` flag (so the restore UI can disable sessions
1511
+ * already visible in the sidebar). Never mutates the registry; never
1512
+ * throws on missing/empty directories.
1513
+ *
1514
+ * Pairs with `handleYeaftRestoreSession` for the "Restore session from a
1515
+ * workdir" flow — see plan rosy-snuggling-waterfall.md.
1516
+ */
1517
+ export function handleYeaftScanWorkdirSessions(msg) {
1518
+ const requestId = msg && msg.requestId;
1519
+ try {
1520
+ const workDir = String(msg && msg.workDir || '').trim();
1521
+ if (!workDir) throw new SessionCrudError('invalid_workdir', null, 'workDir required');
1522
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1523
+ // scanWorkdirSessions is a layer-pure utility — it doesn't read the
1524
+ // central workdir registry. The handler is the right layer to fold in
1525
+ // `alreadyRegistered` because that flag couples the per-workdir scan
1526
+ // to a specific agent's registry (the same scan from a CLI tool or
1527
+ // a different agent would compare against a different registry).
1528
+ const sessions = scanWorkdirSessions(workDir);
1529
+ const registry = readWorkDirRegistry(yeaftDir);
1530
+ const decorated = sessions.map(s => ({
1531
+ ...s,
1532
+ alreadyRegistered: Object.prototype.hasOwnProperty.call(registry, s.id),
1533
+ }));
1534
+ sendSessionCrudResult({ op: 'scan_workdir', requestId, ok: true, sessions: decorated });
1535
+ } catch (err) {
1536
+ sendSessionCrudResult({ op: 'scan_workdir', requestId, ok: false, error: sessionErrorPayload(err) });
1537
+ }
1538
+ }
1539
+
1540
+ /**
1541
+ * `yeaft_restore_session` — register `(sessionId, workDir)` in the
1542
+ * central workdir registry so the next `snapshotSessions()` includes
1543
+ * the session. Validates the session dir exists; rebroadcasts the
1544
+ * snapshot on success so connected sidebars refresh.
1545
+ *
1546
+ * Idempotent: re-restoring an already-registered session succeeds.
1547
+ */
1548
+ export function handleYeaftRestoreSession(msg) {
1549
+ const requestId = msg && msg.requestId;
1550
+ const sessionId = msg && msg.sessionId;
1551
+ const workDir = String(msg && msg.workDir || '').trim();
1552
+ try {
1553
+ if (!sessionId) throw new SessionCrudError('invalid_session_id', null);
1554
+ if (!workDir) throw new SessionCrudError('invalid_workdir', sessionId);
1555
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1556
+ const meta = restoreSessionToRegistry(yeaftDir, sessionId, workDir);
1557
+ sendSessionCrudResult({ op: 'restore', requestId, ok: true, session: meta });
1558
+ sendSessionSnapshotBroadcast();
1559
+ } catch (err) {
1560
+ sendSessionCrudResult({ op: 'restore', requestId, ok: false, error: sessionErrorPayload(err) });
1561
+ }
1562
+ }
1563
+
1504
1564
  export function handleYeaftRenameSession(msg) {
1505
1565
  const requestId = msg && msg.requestId;
1506
1566
  // fix-yeaft-delete-and-agent-revert: accept legacy `groupId` in