@yeaft/webchat-agent 0.1.439 → 0.1.441

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.439",
3
+ "version": "0.1.441",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -20,9 +20,16 @@
20
20
 
21
21
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, renameSync, unlinkSync } from 'fs';
22
22
  import { join, basename } from 'path';
23
+ import { isPermissionError } from '../init.js';
23
24
 
24
25
  // ─── Token estimation ────────────────────────────────────────
25
26
 
27
+ /**
28
+ * Whether a permission warning has already been logged for this store instance.
29
+ * Used to avoid spamming the console with repeated warnings.
30
+ */
31
+ let _permissionWarned = false;
32
+
26
33
  /** Rough token estimation: ~4 chars per token. */
27
34
  export function estimateTokens(text) {
28
35
  if (!text) return 0;
@@ -169,9 +176,20 @@ export class ConversationStore {
169
176
  this.#compactPath = join(dir, 'conversation', 'compact.md');
170
177
  this.#nextSeq = null;
171
178
 
172
- // Ensure directories exist
179
+ // Ensure directories exist (graceful on permission errors)
173
180
  for (const d of [this.#convDir, this.#msgDir, this.#coldDir]) {
174
- if (!existsSync(d)) mkdirSync(d, { recursive: true });
181
+ try {
182
+ if (!existsSync(d)) mkdirSync(d, { recursive: true, mode: 0o755 });
183
+ } catch (err) {
184
+ if (isPermissionError(err)) {
185
+ if (!_permissionWarned) {
186
+ console.warn(`[Yeaft] Cannot create directory ${d}: ${err.code} — persistence disabled`);
187
+ _permissionWarned = true;
188
+ }
189
+ } else {
190
+ throw err;
191
+ }
192
+ }
175
193
  }
176
194
  }
177
195
 
@@ -194,7 +212,18 @@ export class ConversationStore {
194
212
  };
195
213
 
196
214
  const filePath = join(this.#msgDir, `${id}.md`);
197
- writeFileSync(filePath, serializeMessage(fullMsg), 'utf8');
215
+ try {
216
+ writeFileSync(filePath, serializeMessage(fullMsg), { encoding: 'utf8', mode: 0o644 });
217
+ } catch (err) {
218
+ if (isPermissionError(err)) {
219
+ if (!_permissionWarned) {
220
+ console.warn(`[Yeaft] Cannot write message ${id}: ${err.code} — message not persisted`);
221
+ _permissionWarned = true;
222
+ }
223
+ return fullMsg; // Return the message but don't persist
224
+ }
225
+ throw err;
226
+ }
198
227
 
199
228
  this.#nextSeq = seq + 1;
200
229
 
@@ -220,7 +249,18 @@ export class ConversationStore {
220
249
  const src = join(this.#msgDir, `${id}.md`);
221
250
  const dst = join(this.#coldDir, `${id}.md`);
222
251
  if (existsSync(src)) {
223
- renameSync(src, dst);
252
+ try {
253
+ renameSync(src, dst);
254
+ } catch (err) {
255
+ if (isPermissionError(err)) {
256
+ if (!_permissionWarned) {
257
+ console.warn(`[Yeaft] Cannot move message ${id} to cold: ${err.code}`);
258
+ _permissionWarned = true;
259
+ }
260
+ } else {
261
+ throw err;
262
+ }
263
+ }
224
264
  }
225
265
  }
226
266
 
@@ -248,7 +288,18 @@ export class ConversationStore {
248
288
 
249
289
  const date = new Date().toISOString().split('T')[0];
250
290
  const entry = `\n## ${date}\n\n${summary}\n`;
251
- writeFileSync(this.#compactPath, existing + entry, 'utf8');
291
+ try {
292
+ writeFileSync(this.#compactPath, existing + entry, { encoding: 'utf8', mode: 0o644 });
293
+ } catch (err) {
294
+ if (isPermissionError(err)) {
295
+ if (!_permissionWarned) {
296
+ console.warn(`[Yeaft] Cannot write compact summary: ${err.code}`);
297
+ _permissionWarned = true;
298
+ }
299
+ } else {
300
+ throw err;
301
+ }
302
+ }
252
303
  }
253
304
 
254
305
  /**
@@ -285,7 +336,18 @@ export class ConversationStore {
285
336
  'This file tracks the conversation state for the "one eternal conversation" model.',
286
337
  ].join('\n');
287
338
 
288
- writeFileSync(this.#indexPath, content, 'utf8');
339
+ try {
340
+ writeFileSync(this.#indexPath, content, { encoding: 'utf8', mode: 0o644 });
341
+ } catch (err) {
342
+ if (isPermissionError(err)) {
343
+ if (!_permissionWarned) {
344
+ console.warn(`[Yeaft] Cannot write conversation index: ${err.code}`);
345
+ _permissionWarned = true;
346
+ }
347
+ } else {
348
+ throw err;
349
+ }
350
+ }
289
351
  }
290
352
 
291
353
  /**
@@ -296,14 +358,22 @@ export class ConversationStore {
296
358
  if (existsSync(dir)) {
297
359
  for (const file of readdirSync(dir)) {
298
360
  if (file.endsWith('.md')) {
299
- unlinkSync(join(dir, file));
361
+ try {
362
+ unlinkSync(join(dir, file));
363
+ } catch (err) {
364
+ if (!isPermissionError(err)) throw err;
365
+ }
300
366
  }
301
367
  }
302
368
  }
303
369
  }
304
370
  // Reset compact
305
371
  if (existsSync(this.#compactPath)) {
306
- writeFileSync(this.#compactPath, '', 'utf8');
372
+ try {
373
+ writeFileSync(this.#compactPath, '', { encoding: 'utf8', mode: 0o644 });
374
+ } catch (err) {
375
+ if (!isPermissionError(err)) throw err;
376
+ }
307
377
  }
308
378
  this.#nextSeq = 1;
309
379
  this.updateIndex({ totalMessages: 0, lastMessageId: null });
package/unify/engine.js CHANGED
@@ -255,6 +255,7 @@ export class Engine {
255
255
 
256
256
  /**
257
257
  * Persist user message and assistant response to conversation store.
258
+ * Skipped in read-only mode (config._readOnly).
258
259
  *
259
260
  * @param {string} userContent
260
261
  * @param {string} assistantContent
@@ -263,6 +264,7 @@ export class Engine {
263
264
  */
264
265
  #persistMessages(userContent, assistantContent, mode, toolCalls) {
265
266
  if (!this.#conversationStore) return;
267
+ if (this.#config._readOnly) return;
266
268
 
267
269
  // Persist user message
268
270
  this.#conversationStore.append({
@@ -286,11 +288,13 @@ export class Engine {
286
288
 
287
289
  /**
288
290
  * Check and trigger consolidation if needed.
291
+ * Skipped in read-only mode.
289
292
  *
290
293
  * @returns {Promise<{ archivedCount: number, extractedCount: number }|null>}
291
294
  */
292
295
  async #maybeConsolidate() {
293
296
  if (!this.#conversationStore || !this.#memoryStore) return null;
297
+ if (this.#config._readOnly) return null;
294
298
 
295
299
  const budget = this.#config.messageTokenBudget || 8192;
296
300
  if (!shouldConsolidate(this.#conversationStore, budget)) return null;
@@ -520,7 +524,9 @@ export class Engine {
520
524
  yield { type: 'turn_end', turnNumber, stopReason };
521
525
 
522
526
  // ─── Post-query: StopHooks or Legacy ─────────────
523
- if (this.#yeaftDir && this.#conversationStore) {
527
+ if (this.#config._readOnly) {
528
+ // Read-only mode: skip all persistence operations
529
+ } else if (this.#yeaftDir && this.#conversationStore) {
524
530
  // Full pipeline: persist + consolidate + dream gate
525
531
  // Note: stopHooks uses fastConfig for consolidation/dream (cheaper internal tasks)
526
532
  // but receives both configs — messages are persisted with primary model name
package/unify/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Re-exports all public APIs for external consumption.
5
5
  */
6
6
 
7
- export { initYeaftDir, DEFAULT_YEAFT_DIR } from './init.js';
7
+ export { initYeaftDir, DEFAULT_YEAFT_DIR, isWritable, isPermissionError } from './init.js';
8
8
  export { loadConfig, parseFrontmatter, loadMCPConfig } from './config.js';
9
9
  export { DebugTrace, NullTrace, createTrace } from './debug-trace.js';
10
10
  export {
package/unify/init.js CHANGED
@@ -5,10 +5,70 @@
5
5
  * Creates default config.md, MEMORY.md, and conversation/index.md if missing.
6
6
  */
7
7
 
8
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
8
+ import { existsSync, mkdirSync, writeFileSync, accessSync, constants } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { homedir } from 'os';
11
11
 
12
+ /**
13
+ * Check if an error is a permission error (EACCES or EPERM).
14
+ * @param {Error} err
15
+ * @returns {boolean}
16
+ */
17
+ export function isPermissionError(err) {
18
+ return err?.code === 'EACCES' || err?.code === 'EPERM';
19
+ }
20
+
21
+ /**
22
+ * Try to write a file, catching permission errors gracefully.
23
+ * @param {string} filePath
24
+ * @param {string} content
25
+ * @param {string[]} warnings — array to push warning messages into
26
+ */
27
+ function safeWriteFile(filePath, content, warnings) {
28
+ try {
29
+ writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o644 });
30
+ } catch (err) {
31
+ if (isPermissionError(err)) {
32
+ warnings.push(`Cannot write ${filePath}: ${err.code}`);
33
+ } else {
34
+ throw err;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Try to create a directory, catching permission errors gracefully.
41
+ * @param {string} dirPath
42
+ * @param {string[]} warnings — array to push warning messages into
43
+ * @returns {boolean} — true if directory exists (created or already existed)
44
+ */
45
+ function safeMkdir(dirPath, warnings) {
46
+ try {
47
+ mkdirSync(dirPath, { recursive: true, mode: 0o755 });
48
+ return true;
49
+ } catch (err) {
50
+ if (isPermissionError(err)) {
51
+ warnings.push(`Cannot create directory ${dirPath}: ${err.code}`);
52
+ return existsSync(dirPath);
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check if a directory is writable.
60
+ * @param {string} dirPath
61
+ * @returns {boolean}
62
+ */
63
+ export function isWritable(dirPath) {
64
+ try {
65
+ accessSync(dirPath, constants.W_OK);
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
12
72
  /** Default directory for Yeaft data. */
13
73
  export const DEFAULT_YEAFT_DIR = join(homedir(), '.yeaft');
14
74
 
@@ -92,24 +152,34 @@ This file tracks the conversation state for the "one eternal conversation" model
92
152
  * Initialize the Yeaft data directory structure.
93
153
  *
94
154
  * @param {string} [dir] — Root directory path. Defaults to ~/.yeaft/
95
- * @returns {{ dir: string, created: string[] }} — The root dir and list of created paths
155
+ * @returns {{ dir: string, created: string[], writable: boolean, warnings: string[] }} — The root dir, list of created paths, writability status, and any warnings
96
156
  */
97
157
  export function initYeaftDir(dir) {
98
158
  const root = dir || DEFAULT_YEAFT_DIR;
99
159
  const created = [];
160
+ const warnings = [];
100
161
 
101
162
  // Ensure root exists
102
163
  if (!existsSync(root)) {
103
- mkdirSync(root, { recursive: true });
104
- created.push(root);
164
+ if (safeMkdir(root, warnings)) {
165
+ created.push(root);
166
+ }
167
+ }
168
+
169
+ // Check if root is writable early — if not, skip file creation
170
+ const writable = isWritable(root);
171
+ if (!writable) {
172
+ warnings.push(`Directory ${root} is not writable — session will run in read-only mode`);
173
+ return { dir: root, created, writable, warnings };
105
174
  }
106
175
 
107
176
  // Ensure all subdirectories exist
108
177
  for (const sub of SUBDIRS) {
109
178
  const fullPath = join(root, sub);
110
179
  if (!existsSync(fullPath)) {
111
- mkdirSync(fullPath, { recursive: true });
112
- created.push(fullPath);
180
+ if (safeMkdir(fullPath, warnings)) {
181
+ created.push(fullPath);
182
+ }
113
183
  }
114
184
  }
115
185
 
@@ -117,28 +187,28 @@ export function initYeaftDir(dir) {
117
187
  // config.json — default configuration (user edits this directly)
118
188
  const configJsonPath = join(root, 'config.json');
119
189
  if (!existsSync(configJsonPath)) {
120
- writeFileSync(configJsonPath, DEFAULT_CONFIG_JSON, 'utf8');
190
+ safeWriteFile(configJsonPath, DEFAULT_CONFIG_JSON, warnings);
121
191
  created.push(configJsonPath);
122
192
  }
123
193
 
124
194
  const memoryPath = join(root, 'memory', 'MEMORY.md');
125
195
  if (!existsSync(memoryPath)) {
126
- writeFileSync(memoryPath, DEFAULT_MEMORY, 'utf8');
196
+ safeWriteFile(memoryPath, DEFAULT_MEMORY, warnings);
127
197
  created.push(memoryPath);
128
198
  }
129
199
 
130
200
  const indexPath = join(root, 'conversation', 'index.md');
131
201
  if (!existsSync(indexPath)) {
132
- writeFileSync(indexPath, DEFAULT_CONVERSATION_INDEX, 'utf8');
202
+ safeWriteFile(indexPath, DEFAULT_CONVERSATION_INDEX, warnings);
133
203
  created.push(indexPath);
134
204
  }
135
205
 
136
206
  // mcp.json.example — reference template for MCP server configuration
137
207
  const mcpExamplePath = join(root, 'mcp.json.example');
138
208
  if (!existsSync(mcpExamplePath)) {
139
- writeFileSync(mcpExamplePath, DEFAULT_MCP_EXAMPLE, 'utf8');
209
+ safeWriteFile(mcpExamplePath, DEFAULT_MCP_EXAMPLE, warnings);
140
210
  created.push(mcpExamplePath);
141
211
  }
142
212
 
143
- return { dir: root, created };
213
+ return { dir: root, created, writable, warnings };
144
214
  }
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from 'fs';
27
27
  import { join, basename } from 'path';
28
+ import { isPermissionError } from '../init.js';
28
29
 
29
30
  // ─── Constants ──────────────────────────────────────────────────
30
31
 
@@ -37,6 +38,9 @@ export const MAX_ENTRIES = 200;
37
38
  /** Maximum MEMORY.md line count. */
38
39
  export const MAX_MEMORY_LINES = 200;
39
40
 
41
+ /** Whether a permission warning has been logged for this store. */
42
+ let _permissionWarned = false;
43
+
40
44
  // ─── Entry Parsing ──────────────────────────────────────────────
41
45
 
42
46
  /**
@@ -157,9 +161,20 @@ export class MemoryStore {
157
161
  this.#memoryPath = join(dir, 'memory', 'MEMORY.md');
158
162
  this.#scopesPath = join(dir, 'memory', 'scopes.md');
159
163
 
160
- // Ensure directories exist
164
+ // Ensure directories exist (graceful on permission errors)
161
165
  for (const d of [this.#memoryDir, this.#entriesDir]) {
162
- if (!existsSync(d)) mkdirSync(d, { recursive: true });
166
+ try {
167
+ if (!existsSync(d)) mkdirSync(d, { recursive: true, mode: 0o755 });
168
+ } catch (err) {
169
+ if (isPermissionError(err)) {
170
+ if (!_permissionWarned) {
171
+ console.warn(`[Yeaft] Cannot create directory ${d}: ${err.code} — memory persistence disabled`);
172
+ _permissionWarned = true;
173
+ }
174
+ } else {
175
+ throw err;
176
+ }
177
+ }
163
178
  }
164
179
  }
165
180
 
@@ -179,7 +194,18 @@ export class MemoryStore {
179
194
  * @param {string} content
180
195
  */
181
196
  writeProfile(content) {
182
- writeFileSync(this.#memoryPath, content, 'utf8');
197
+ try {
198
+ writeFileSync(this.#memoryPath, content, { encoding: 'utf8', mode: 0o644 });
199
+ } catch (err) {
200
+ if (isPermissionError(err)) {
201
+ if (!_permissionWarned) {
202
+ console.warn(`[Yeaft] Cannot write MEMORY.md: ${err.code}`);
203
+ _permissionWarned = true;
204
+ }
205
+ } else {
206
+ throw err;
207
+ }
208
+ }
183
209
  }
184
210
 
185
211
  /**
@@ -286,7 +312,18 @@ export class MemoryStore {
286
312
  lines.push(`| ${scope} | ${info.count} | ${info.lastUpdated} |`);
287
313
  }
288
314
 
289
- writeFileSync(this.#scopesPath, lines.join('\n') + '\n', 'utf8');
315
+ try {
316
+ writeFileSync(this.#scopesPath, lines.join('\n') + '\n', { encoding: 'utf8', mode: 0o644 });
317
+ } catch (err) {
318
+ if (isPermissionError(err)) {
319
+ if (!_permissionWarned) {
320
+ console.warn(`[Yeaft] Cannot write scopes.md: ${err.code}`);
321
+ _permissionWarned = true;
322
+ }
323
+ } else {
324
+ throw err;
325
+ }
326
+ }
290
327
  }
291
328
 
292
329
  // ─── Entries CRUD ─────────────────────────────────────
@@ -342,7 +379,18 @@ export class MemoryStore {
342
379
  };
343
380
 
344
381
  const filePath = join(this.#entriesDir, `${slug}.md`);
345
- writeFileSync(filePath, serializeEntry(fullEntry), 'utf8');
382
+ try {
383
+ writeFileSync(filePath, serializeEntry(fullEntry), { encoding: 'utf8', mode: 0o644 });
384
+ } catch (err) {
385
+ if (isPermissionError(err)) {
386
+ if (!_permissionWarned) {
387
+ console.warn(`[Yeaft] Cannot write memory entry ${slug}: ${err.code}`);
388
+ _permissionWarned = true;
389
+ }
390
+ return slug; // Return slug but don't persist
391
+ }
392
+ throw err;
393
+ }
346
394
 
347
395
  return slug;
348
396
  }
@@ -378,7 +426,18 @@ export class MemoryStore {
378
426
  entry.frequency = (entry.frequency || 1) + 1;
379
427
  entry.updated_at = new Date().toISOString();
380
428
  const filePath = join(this.#entriesDir, `${name}.md`);
381
- writeFileSync(filePath, serializeEntry(entry), 'utf8');
429
+ try {
430
+ writeFileSync(filePath, serializeEntry(entry), { encoding: 'utf8', mode: 0o644 });
431
+ } catch (err) {
432
+ if (isPermissionError(err)) {
433
+ if (!_permissionWarned) {
434
+ console.warn(`[Yeaft] Cannot bump frequency for ${name}: ${err.code}`);
435
+ _permissionWarned = true;
436
+ }
437
+ } else {
438
+ throw err;
439
+ }
440
+ }
382
441
  }
383
442
 
384
443
  // ─── Search / Filter ──────────────────────────────────
@@ -489,19 +548,31 @@ export class MemoryStore {
489
548
  if (existsSync(this.#entriesDir)) {
490
549
  for (const file of readdirSync(this.#entriesDir)) {
491
550
  if (file.endsWith('.md')) {
492
- unlinkSync(join(this.#entriesDir, file));
551
+ try {
552
+ unlinkSync(join(this.#entriesDir, file));
553
+ } catch (err) {
554
+ if (!isPermissionError(err)) throw err;
555
+ }
493
556
  }
494
557
  }
495
558
  }
496
559
 
497
560
  // Clear MEMORY.md
498
561
  if (existsSync(this.#memoryPath)) {
499
- writeFileSync(this.#memoryPath, '', 'utf8');
562
+ try {
563
+ writeFileSync(this.#memoryPath, '', { encoding: 'utf8', mode: 0o644 });
564
+ } catch (err) {
565
+ if (!isPermissionError(err)) throw err;
566
+ }
500
567
  }
501
568
 
502
569
  // Clear scopes.md
503
570
  if (existsSync(this.#scopesPath)) {
504
- unlinkSync(this.#scopesPath);
571
+ try {
572
+ unlinkSync(this.#scopesPath);
573
+ } catch (err) {
574
+ if (!isPermissionError(err)) throw err;
575
+ }
505
576
  }
506
577
  }
507
578
  }
package/unify/session.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * a fully wired Session ready for queries.
14
14
  */
15
15
 
16
- import { initYeaftDir, DEFAULT_YEAFT_DIR } from './init.js';
16
+ import { initYeaftDir, DEFAULT_YEAFT_DIR, isWritable } from './init.js';
17
17
  import { loadConfig, loadMCPConfig } from './config.js';
18
18
  import { createTrace } from './debug-trace.js';
19
19
  import { createLLMAdapter } from './llm/adapter.js';
@@ -85,12 +85,25 @@ export async function loadSession(options = {}) {
85
85
  if (debug !== undefined) overrides.debug = debug;
86
86
 
87
87
  const yeaftDir = overrides.dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
88
- initYeaftDir(yeaftDir);
88
+ const initResult = initYeaftDir(yeaftDir);
89
89
  overrides.dir = yeaftDir;
90
90
 
91
+ // Log any warnings from directory initialization
92
+ for (const w of initResult.warnings) {
93
+ console.warn(`[Yeaft] ${w}`);
94
+ }
95
+
91
96
  // ─── 2. Load config ───────────────────────────────────
92
97
  const config = loadConfig(overrides);
93
98
 
99
+ // ─── 2a. Permission pre-check ─────────────────────────
100
+ // If the data dir is not writable, mark session as read-only.
101
+ // Persistence (conversation, memory, dream) is skipped in this mode.
102
+ if (!initResult.writable) {
103
+ config._readOnly = true;
104
+ console.warn(`[Yeaft] ${yeaftDir} is not writable — running in read-only mode`);
105
+ }
106
+
94
107
  // ─── 3. Create debug trace ─────────────────────────────
95
108
  const trace = createTrace({
96
109
  enabled: config.debug,
@@ -12,6 +12,10 @@
12
12
 
13
13
  import { shouldConsolidate, consolidate } from './memory/consolidate.js';
14
14
  import { checkDreamGate, incrementQueryCount, dream } from './memory/dream.js';
15
+ import { isPermissionError } from './init.js';
16
+
17
+ /** Track whether we've already warned about permission issues in stop hooks. */
18
+ let _permissionWarned = false;
15
19
 
16
20
  /**
17
21
  * Run all stop hooks after a query completes.
@@ -77,7 +81,14 @@ export async function runStopHooks(context) {
77
81
  }
78
82
  }
79
83
  } catch (err) {
80
- result.errors.push(`Persist failed: ${err.message}`);
84
+ if (isPermissionError(err)) {
85
+ if (!_permissionWarned) {
86
+ result.errors.push('Cannot write to ~/.yeaft/ — check directory permissions');
87
+ _permissionWarned = true;
88
+ }
89
+ } else {
90
+ result.errors.push(`Persist failed: ${err.message}`);
91
+ }
81
92
  }
82
93
 
83
94
  // 2. Consolidate check (non-blocking, but awaited for correctness)
@@ -102,7 +113,14 @@ export async function runStopHooks(context) {
102
113
  }
103
114
  }
104
115
  } catch (err) {
105
- result.errors.push(`Consolidate failed: ${err.message}`);
116
+ if (isPermissionError(err)) {
117
+ if (!_permissionWarned) {
118
+ result.errors.push('Cannot write to ~/.yeaft/ — consolidation skipped');
119
+ _permissionWarned = true;
120
+ }
121
+ } else {
122
+ result.errors.push(`Consolidate failed: ${err.message}`);
123
+ }
106
124
  }
107
125
 
108
126
  // 3. Increment dream query counter
@@ -111,7 +129,11 @@ export async function runStopHooks(context) {
111
129
  incrementQueryCount(yeaftDir);
112
130
  }
113
131
  } catch (err) {
114
- result.errors.push(`Dream counter failed: ${err.message}`);
132
+ if (isPermissionError(err)) {
133
+ // Silent — already warned about permission issues
134
+ } else {
135
+ result.errors.push(`Dream counter failed: ${err.message}`);
136
+ }
115
137
  }
116
138
 
117
139
  // 4. Dream gate check (fire-and-forget, background)
@@ -136,7 +158,9 @@ export async function runStopHooks(context) {
136
158
  }
137
159
  }
138
160
  } catch (err) {
139
- result.errors.push(`Dream gate check failed: ${err.message}`);
161
+ if (!isPermissionError(err)) {
162
+ result.errors.push(`Dream gate check failed: ${err.message}`);
163
+ }
140
164
  }
141
165
 
142
166
  return result;
@@ -32,6 +32,20 @@ let unifyConversationId = null;
32
32
  /** Current query mode: 'chat' or 'work' */
33
33
  let currentMode = 'chat';
34
34
 
35
+ /** Whether we've already sent a permission warning to the UI */
36
+ let _permissionDiagnosticSent = false;
37
+
38
+ /**
39
+ * Check if an error message is a permission error.
40
+ * @param {string} msg
41
+ * @returns {boolean}
42
+ */
43
+ function isPermissionErrorMsg(msg) {
44
+ if (!msg) return false;
45
+ const lower = msg.toLowerCase();
46
+ return lower.includes('eacces') || lower.includes('eperm') || lower.includes('permission denied');
47
+ }
48
+
35
49
  /**
36
50
  * Send a unify_output message carrying claude_output-format data.
37
51
  * The server forwards this as-is to the web client.
@@ -251,17 +265,36 @@ export async function handleUnifyChat(msg) {
251
265
  break;
252
266
 
253
267
  // ── Errors ──
254
- case 'error':
255
- sendUnifyOutput({
256
- type: 'assistant',
257
- message: {
258
- content: [{
259
- type: 'text',
260
- text: `⚠️ Error: ${event.error?.message || 'Unknown error'}`,
261
- }],
262
- },
263
- });
268
+ case 'error': {
269
+ const errMsg = event.error?.message || 'Unknown error';
270
+ // Filter permission errors: show friendly one-time diagnostic instead of raw error
271
+ if (isPermissionErrorMsg(errMsg)) {
272
+ if (!_permissionDiagnosticSent) {
273
+ _permissionDiagnosticSent = true;
274
+ sendUnifyOutput({
275
+ type: 'assistant',
276
+ message: {
277
+ content: [{
278
+ type: 'text',
279
+ text: '⚠️ Cannot write to ~/.yeaft/ directory — some features (memory, history) are unavailable. Please check directory permissions: `chmod -R u+rw ~/.yeaft/`',
280
+ }],
281
+ },
282
+ });
283
+ }
284
+ // Don't show subsequent permission errors
285
+ } else {
286
+ sendUnifyOutput({
287
+ type: 'assistant',
288
+ message: {
289
+ content: [{
290
+ type: 'text',
291
+ text: `⚠️ Error: ${errMsg}`,
292
+ }],
293
+ },
294
+ });
295
+ }
264
296
  break;
297
+ }
265
298
 
266
299
  default:
267
300
  // Silently consume unknown events
@@ -306,15 +339,32 @@ export async function handleUnifyChat(msg) {
306
339
  }
307
340
 
308
341
  console.error('[Unify] query error:', err.message);
309
- sendUnifyOutput({
310
- type: 'assistant',
311
- message: {
312
- content: [{
313
- type: 'text',
314
- text: `⚠️ Session error: ${err.message}`,
315
- }],
316
- },
317
- });
342
+
343
+ // Filter permission errors at the session level too
344
+ if (isPermissionErrorMsg(err.message)) {
345
+ if (!_permissionDiagnosticSent) {
346
+ _permissionDiagnosticSent = true;
347
+ sendUnifyOutput({
348
+ type: 'assistant',
349
+ message: {
350
+ content: [{
351
+ type: 'text',
352
+ text: '⚠️ Cannot write to ~/.yeaft/ directory — some features (memory, history) are unavailable. Please check directory permissions: `chmod -R u+rw ~/.yeaft/`',
353
+ }],
354
+ },
355
+ });
356
+ }
357
+ } else {
358
+ sendUnifyOutput({
359
+ type: 'assistant',
360
+ message: {
361
+ content: [{
362
+ type: 'text',
363
+ text: `⚠️ Session error: ${err.message}`,
364
+ }],
365
+ },
366
+ });
367
+ }
318
368
  // Still send result to clear processing state
319
369
  sendUnifyOutput({
320
370
  type: 'result',