@yeaft/webchat-agent 0.1.780 → 0.1.785

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.780",
3
+ "version": "0.1.785",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/session.js CHANGED
@@ -50,7 +50,7 @@ import { openSegmentIndex } from './memory/index-db.js';
50
50
  import { syncAll as syncSegmentIndex } from './memory/segment-sync.js';
51
51
  import { openAmsRegistry } from './memory/ams-registry.js';
52
52
  import { join } from 'path';
53
- import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from 'fs';
53
+ import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe, mkdirSync as mkdirSyncSafe } from 'fs';
54
54
 
55
55
  /**
56
56
  * @typedef {Object} SessionOptions
@@ -80,6 +80,33 @@ import { existsSync as existsSyncSafe, readFileSync as readFileSyncSafe } from '
80
80
  * @property {() => Promise<void>} shutdown — Graceful shutdown
81
81
  */
82
82
 
83
+ /**
84
+ * Eagerly create `<yeaftDir>/stats/` and surface failures as a warn.
85
+ *
86
+ * Returns the resolved path either way — the caller can still pass it
87
+ * to `ToolUsageStats`, which keeps an in-memory counter path even when
88
+ * the disk is read-only.
89
+ *
90
+ * NOTE: this knowledge ("stats lives under `stats/`") belongs inside
91
+ * `ToolUsageStats.init()`. Once that exists, delete this helper and
92
+ * the call site collapses to `await toolStats.init(yeaftDir)`.
93
+ *
94
+ * @param {string} yeaftDir
95
+ * @returns {string} statsDir
96
+ */
97
+ function prepareToolStatsDir(yeaftDir) {
98
+ const statsDir = join(yeaftDir, 'stats');
99
+ try {
100
+ mkdirSyncSafe(statsDir, { recursive: true });
101
+ } catch (err) {
102
+ console.warn(
103
+ `[Unify] Could not create stats dir ${statsDir}: ${err?.message || err}. ` +
104
+ `Tool-usage counters will live in memory only.`
105
+ );
106
+ }
107
+ return statsDir;
108
+ }
109
+
83
110
  /**
84
111
  * Load (or initialize) a Yeaft session.
85
112
  *
@@ -320,8 +347,24 @@ export async function loadSession(options = {}) {
320
347
  // Tool-call usage statistics: persisted to <yeaftDir>/stats/tool-usage.json.
321
348
  // Loaded synchronously at boot so the first turn already sees prior counts.
322
349
  // Threaded into the engine so it can `record` each tool_exec event.
350
+ //
351
+ // 2026-05-16: eagerly create the `stats/` directory at boot. The
352
+ // ToolUsageStats writer does `fsp.mkdir(..., {recursive:true})` lazily
353
+ // inside `#doFlush()` and swallows any mkdir error, which meant an
354
+ // unwritable parent (perm denied, ENOSPC) was silently invisible
355
+ // until the user filed a support ticket. Doing it here surfaces the
356
+ // failure as a console.warn while still leaving the in-memory
357
+ // counter path functional — the engine keeps recording even if the
358
+ // disk is read-only.
359
+ //
360
+ // FOLLOW-UP: this leaks `ToolUsageStats`'s storage layout (its
361
+ // directory name) into the session orchestrator. The right home is
362
+ // a `ToolUsageStats.init()` that owns the mkdir + the warn + a
363
+ // `writesDisabled` flag. Tracking as future work; for now the helper
364
+ // below visually quarantines the leak so the migration is one delete.
365
+ const statsDir = prepareToolStatsDir(yeaftDir);
323
366
  const toolStats = new ToolUsageStats({
324
- path: join(yeaftDir, 'stats', 'tool-usage.json'),
367
+ path: join(statsDir, 'tool-usage.json'),
325
368
  });
326
369
  toolStats.loadSync();
327
370
  const engine = new Engine({
@@ -1,8 +1,9 @@
1
1
  # Planning Mode
2
2
 
3
- You have just entered **planning mode** for the topic below. Do NOT start
4
- executing yet. Your job in this turn is to **think through the work and produce
5
- a concrete plan**, then hand it off to `TodoWrite` so the steps are tracked.
3
+ You have just entered **planning mode** for the topic below. Your job is to
4
+ **think through the work, land a concrete plan via `TodoWrite`, and then
5
+ continue executing the first step in the same turn**. Don't stop after writing
6
+ the plan — keep going.
6
7
 
7
8
  ## How to think
8
9
 
@@ -20,7 +21,7 @@ a concrete plan**, then hand it off to `TodoWrite` so the steps are tracked.
20
21
 
21
22
  ## Output shape
22
23
 
23
- Reply in two parts:
24
+ Reply in three parts, all in the **same turn**:
24
25
 
25
26
  **Part 1 — Plan (prose, short).** 5–10 lines covering the problem, the chosen
26
27
  approach, and the key risks. No filler. Skip if the topic is trivial.
@@ -32,9 +33,15 @@ array. Status rule:
32
33
  - Use the **imperative** form for `content` ("Write failing test"), and the
33
34
  **present-continuous** form for `activeForm` ("Writing failing test").
34
35
 
35
- **Do not execute the steps in this turn.** This turn ends after the `TodoWrite`
36
- call returns. On the next turn, the user (or you) will pick up the
37
- `in_progress` item and start work.
36
+ **Part 3 Start executing the first step.** Immediately after `TodoWrite`
37
+ returns, begin work on the `in_progress` item by calling whatever tool(s) that
38
+ step needs (bash, file-edit, grep, etc.). Don't end the turn just because the
39
+ plan is written — the plan is the runway, not the destination.
40
+
41
+ **Exception — stop after the plan only if** the very first step is "ask the
42
+ user / wait for input" (e.g. an unresolved unknown that genuinely blocks
43
+ every other step). In that case, call the `ask_user` tool with the
44
+ question and end the turn. Otherwise keep moving.
38
45
 
39
46
  ## Tone
40
47
 
@@ -3,11 +3,14 @@
3
3
  *
4
4
  * Lightweight planning entry point inspired by Claude Code's plan mode.
5
5
  * Unlike Claude Code, we do NOT swap tools or change conversation state —
6
- * `StartPlan` is a regular tool. Its only job is to push a planning
7
- * instruction back into the model's tool-result stream so the very next
8
- * turn produces a structured plan plus a `TodoWrite` call.
6
+ * `StartPlan` is a regular tool. Its job is to push a planning instruction
7
+ * back into the model's tool-result stream so the same turn produces a
8
+ * short prose plan, a `TodoWrite` call to land the steps, and then keeps
9
+ * going by starting the first step. The plan is the runway, not the
10
+ * destination — the loop continues until the work is done (or until the
11
+ * model legitimately needs the user, e.g. an unresolved unknown).
9
12
  *
10
- * Design (locked 2026-05-13):
13
+ * Design:
11
14
  * - Anyone can call it; the tool description tells the LLM when to.
12
15
  * - The instruction text comes from one of two places, in order:
13
16
  * 1. The active VP's `planInstruction` frontmatter override
@@ -20,13 +23,16 @@
20
23
  * are echoed back in the tool result so the planning turn can read
21
24
  * them without re-asking the user.
22
25
  * - Output is plain text (the instruction + the echo). No side effects,
23
- * no persistence — the LLM's next turn does the actual planning and
24
- * calls TodoWrite to land structured steps.
26
+ * no persistence — the same turn that called StartPlan produces the
27
+ * plan, lands the steps via TodoWrite, and starts executing the first
28
+ * step (unless that first step is "ask the user", which is the one
29
+ * legitimate reason to stop here).
25
30
  *
26
- * The expected integration is TodoWrite: after the planning turn the LLM
27
- * issues a `TodoWrite` call enumerating the 1..N steps. The frontend
28
- * already renders TodoWrite as a checkbox-style list, so the user sees
29
- * the plan materialize without any new UI.
31
+ * The expected integration is TodoWrite: during the planning turn the LLM
32
+ * issues a `TodoWrite` call enumerating the 1..N steps, then keeps going
33
+ * and works the first step. The frontend already renders TodoWrite as a
34
+ * checkbox-style list, so the user sees the plan materialize without any
35
+ * new UI.
30
36
  */
31
37
 
32
38
  import { defineTool } from './types.js';
@@ -36,9 +42,10 @@ export default defineTool({
36
42
  name: 'StartPlan',
37
43
  description: `Enter planning mode for a non-trivial task. Use BEFORE you start working when the request needs multiple steps, has unclear scope, or the user said "make a plan" / "think through this first".
38
44
 
39
- This tool does NOT execute the work. It returns a planning instruction; on the next turn you should:
45
+ This tool returns a planning instruction. Use it to land a structured plan, then keep working in the same turn. The expected flow is:
40
46
  1. Produce a short prose plan (problem, approach, risks).
41
47
  2. Call \`TodoWrite\` with the ordered steps. Mark the first concrete step "in_progress", the rest "pending".
48
+ 3. Start executing that first step — call whatever tools the work needs. Do not end the turn just because the plan is written; the plan is the runway.
42
49
 
43
50
  WHEN TO USE:
44
51
  - Multi-step implementation (3+ steps), refactor, or open-ended investigation.
@@ -49,6 +56,8 @@ WHEN NOT TO USE:
49
56
  - Single trivial change, single command run, lookup-style question.
50
57
  - Mid-execution — once you're past the first step, use TodoWrite directly.
51
58
 
59
+ EXCEPTION — stop after the plan only if the first step is genuinely "ask the user" (an unresolved unknown that blocks every other step). Otherwise keep moving.
60
+
52
61
  The tool takes the topic plus optional guiding fields (stuckAt, userProblem, expectedScale, additionalContext) that help you think; they're echoed back verbatim, so don't repeat the full user request in \`topic\`.`,
53
62
  parameters: {
54
63
  type: 'object',
@@ -126,7 +135,7 @@ The tool takes the topic plus optional guiding fields (stuckAt, userProblem, exp
126
135
  lines.push('</guiding-context>');
127
136
  }
128
137
  lines.push('');
129
- lines.push('Next: produce the plan as described above, then call `TodoWrite` to land the ordered steps. Do NOT start executing the steps in this turn.');
138
+ lines.push('Next: produce the plan, call `TodoWrite` to land the ordered steps, then keep going — start executing the first step in this same turn. Only stop after the plan if the first step is "ask the user".');
130
139
 
131
140
  return lines.join('\n');
132
141
  },