@yemi33/minions 0.1.2450 → 0.1.2451

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.
@@ -19,13 +19,44 @@ function _timeSinceMs(mtimeMs) {
19
19
  return days + 'd ago';
20
20
  }
21
21
 
22
+ // Per-file content cache keyed by name → { mtimeMs, content }. The inbox can
23
+ // hold thousands of notes; without a cache every 4 s refresh re-fetched EVERY
24
+ // file's body. Firing that many fetches at once exhausted the browser socket
25
+ // pool and flooded the console with `net::ERR_INSUFFICIENT_RESOURCES` on every
26
+ // page (the refresh loop runs regardless of the active page). Caching by
27
+ // (name, mtimeMs) means steady-state refreshes re-fetch nothing — only new or
28
+ // edited notes are pulled — and the pool below bounds the initial load so the
29
+ // socket pool is never saturated. (W-mscjlch1005qdc11)
30
+ const _inboxContentCache = new Map();
31
+ // Max concurrent /state/notes/inbox/<file> fetches. Chrome caps same-host HTTP/1
32
+ // connections at 6; 8 keeps the pipe full without tripping ERR_INSUFFICIENT_RESOURCES.
33
+ const INBOX_FETCH_CONCURRENCY = 8;
34
+
35
+ // Run async `worker(item)` over `items` with at most `limit` in flight at once.
36
+ // Preserves input order in the returned results array. Used to bound the inbox
37
+ // per-file content fetches so a large inbox can't exhaust the browser's socket pool.
38
+ async function _mapWithConcurrency(items, limit, worker) {
39
+ const results = new Array(items.length);
40
+ let next = 0;
41
+ async function run() {
42
+ while (next < items.length) {
43
+ const i = next++;
44
+ results[i] = await worker(items[i], i);
45
+ }
46
+ }
47
+ const pool = [];
48
+ for (let i = 0; i < Math.min(limit, items.length); i++) pool.push(run());
49
+ await Promise.all(pool);
50
+ return results;
51
+ }
52
+
22
53
  // Fetch the inbox via the /state/notes/inbox directory listing + per-file
23
54
  // content fetch. The listing call returns {entries:[{name, mtimeMs, size,
24
- // isDir}]} and is cheap (one statSync per entry). The per-file fetches run
25
- // in parallel with their own mtime+size ETag so unchanged files 304 with
26
- // no body. Issue #2949 the staleness fix applies here too: the inbox
27
- // directory mtime advances on every new note, and per-file ETags surface
28
- // content edits within one 4 s poll.
55
+ // isDir}]} and is cheap (one statSync per entry). Per-file bodies are fetched
56
+ // through a bounded-concurrency pool and cached by (name, mtimeMs), so unchanged
57
+ // notes are served from memory and only new/edited notes hit the network. Issue
58
+ // #2949 the staleness fix applies here too: the inbox directory mtime advances
59
+ // on every new note, and the mtime cache key surfaces content edits within one 4 s poll.
29
60
  async function fetchInboxFromDisk() {
30
61
  try {
31
62
  const listResp = await fetch('/state/notes/inbox');
@@ -35,21 +66,27 @@ async function fetchInboxFromDisk() {
35
66
  const mdFiles = entries
36
67
  .filter((e) => e && !e.isDir && typeof e.name === 'string' && e.name.endsWith('.md'))
37
68
  .sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
38
- const items = await Promise.all(mdFiles.map(async (e) => {
69
+ // Drop cache entries for notes that no longer exist so the cache can't grow
70
+ // unbounded across the lifetime of the page.
71
+ const liveNames = new Set(mdFiles.map((e) => e.name));
72
+ for (const key of _inboxContentCache.keys()) {
73
+ if (!liveNames.has(key)) _inboxContentCache.delete(key);
74
+ }
75
+ const items = await _mapWithConcurrency(mdFiles, INBOX_FETCH_CONCURRENCY, async (e) => {
76
+ const cached = _inboxContentCache.get(e.name);
77
+ if (cached && cached.mtimeMs === e.mtimeMs) {
78
+ return { name: e.name, mtime: e.mtimeMs, age: _timeSinceMs(e.mtimeMs), content: cached.content };
79
+ }
39
80
  try {
40
81
  const r = await fetch('/state/notes/inbox/' + encodeURIComponent(e.name));
41
82
  if (!r.ok) return null;
42
83
  const content = await r.text();
43
- return {
44
- name: e.name,
45
- mtime: e.mtimeMs,
46
- age: _timeSinceMs(e.mtimeMs),
47
- content,
48
- };
84
+ _inboxContentCache.set(e.name, { mtimeMs: e.mtimeMs, content });
85
+ return { name: e.name, mtime: e.mtimeMs, age: _timeSinceMs(e.mtimeMs), content };
49
86
  } catch {
50
87
  return null;
51
88
  }
52
- }));
89
+ });
53
90
  return items.filter(Boolean);
54
91
  } catch {
55
92
  return null;
package/dashboard.js CHANGED
@@ -3690,6 +3690,19 @@ function handleStateRead(req, res) {
3690
3690
  let lstat;
3691
3691
  try { lstat = fs.lstatSync(resolved); }
3692
3692
  catch {
3693
+ // A not-yet-created but allowlisted top-level state directory (e.g. the
3694
+ // meetings dir before the first meeting is recorded) is an EMPTY directory,
3695
+ // not a missing resource. Returning 404 here made the dashboard's directory-
3696
+ // listing clients (render-meetings.js) log a console error on every refresh.
3697
+ // List it as empty instead so "no items yet" is a clean 200. Missing FILES
3698
+ // and missing nested paths still 404. (W-mscjlch1005qdc11)
3699
+ if (isSingleSegment && STATE_READ_ALLOWED_DIRS.has(top)) {
3700
+ res.statusCode = 200;
3701
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
3702
+ res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
3703
+ res.end(JSON.stringify({ path: rel.replace(/\\/g, '/'), entries: [] }));
3704
+ return;
3705
+ }
3693
3706
  res.statusCode = 404;
3694
3707
  res.setHeader('Content-Type', 'application/json');
3695
3708
  res.end(JSON.stringify({ error: 'not found' }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2450",
3
+ "version": "0.1.2451",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"