@yemi33/minions 0.1.2033 → 0.1.2035

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.
Files changed (2) hide show
  1. package/engine/queries.js +100 -6
  2. package/package.json +1 -1
package/engine/queries.js CHANGED
@@ -1798,13 +1798,76 @@ function _projectGitStatusCacheKey(localPath, configuredMainBranch) {
1798
1798
  return norm + '::' + (configuredMainBranch ? String(configuredMainBranch).trim() : '');
1799
1799
  }
1800
1800
 
1801
+ // Resolve the absolute `.git` directory for `localPath`, transparently
1802
+ // handling linked worktrees (where `<localPath>/.git` is a *file* whose
1803
+ // first line reads `gitdir: <abs path>`). Returns null when no valid git
1804
+ // linkage exists. Synchronous and cheap — one statSync + at most one
1805
+ // short readFileSync. Used by both `_projectGitRefsAdvancedSince` and the
1806
+ // fast-state mtime tracker so the same set of ref files is consulted in
1807
+ // both places, including for linked-worktree repos.
1808
+ function _resolveGitDir(localPath) {
1809
+ if (!localPath) return null;
1810
+ const gitPath = path.join(localPath, '.git');
1811
+ let st;
1812
+ try { st = fs.statSync(gitPath); }
1813
+ catch { return null; }
1814
+ if (st.isDirectory()) return gitPath;
1815
+ if (st.isFile()) {
1816
+ let head = '';
1817
+ try { head = fs.readFileSync(gitPath, { encoding: 'utf8', flag: 'r' }).slice(0, 4096); }
1818
+ catch { return null; }
1819
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(head);
1820
+ if (!m) return null;
1821
+ const target = m[1];
1822
+ return path.isAbsolute(target) ? target : path.resolve(localPath, target);
1823
+ }
1824
+ return null;
1825
+ }
1826
+
1827
+ // Return true when any of the per-project git ref files (logs/HEAD,
1828
+ // FETCH_HEAD, refs/remotes/origin/<comparator>) have mtimeMs > cachedTs.
1829
+ // Lets `getProjectGitStatus` bypass its 15s TTL after `git pull`, `git
1830
+ // fetch`, `git checkout`, etc. so the next /api/status reflects the new
1831
+ // HEAD / ahead-behind within one SPA poll instead of waiting out the TTL
1832
+ // (W-mphdmr8c00030124). Tolerates ENOENT on FETCH_HEAD / refs (never-
1833
+ // fetched repos simply haven't moved those files yet). Cost ≤3 statSync
1834
+ // per project per /api/status build — well under the 'cheap' budget
1835
+ // called out in getStatusFastStateMtimePaths's docstring.
1836
+ function _projectGitRefsAdvancedSince(localPath, cachedTs, configuredMainBranch) {
1837
+ const gitDir = _resolveGitDir(localPath);
1838
+ if (!gitDir) return false;
1839
+ const candidates = [
1840
+ path.join(gitDir, 'logs', 'HEAD'),
1841
+ path.join(gitDir, 'FETCH_HEAD'),
1842
+ ];
1843
+ const comparator = configuredMainBranch && String(configuredMainBranch).trim();
1844
+ if (comparator) {
1845
+ candidates.push(path.join(gitDir, 'refs', 'remotes', 'origin', comparator));
1846
+ }
1847
+ for (const file of candidates) {
1848
+ try {
1849
+ const st = fs.statSync(file);
1850
+ if (st.mtimeMs > cachedTs) return true;
1851
+ } catch { /* ENOENT / EPERM — file just hasn't moved */ }
1852
+ }
1853
+ return false;
1854
+ }
1855
+
1801
1856
  function getProjectGitStatus(localPath, configuredMainBranch = null) {
1802
1857
  const norm = String(localPath || '').replace(/\\/g, '/');
1803
1858
  if (!norm) return PROJECT_GIT_STATUS_MISSING;
1804
1859
  const key = _projectGitStatusCacheKey(localPath, configuredMainBranch);
1805
1860
  const now = Date.now();
1806
1861
  const cached = _projectGitStatusCache.get(key);
1807
- if (cached && cached.ts && (now - cached.ts) < PROJECT_GIT_STATUS_TTL) return cached.value;
1862
+ // Within TTL: short-circuit ONLY when no tracked git ref has advanced
1863
+ // past cached.ts. Without the mtime gate, a freshly-pulled repo serves
1864
+ // the pre-pull ahead/behind counts for up to 15s + one SPA poll (~19s
1865
+ // user-visible lag) because the rebuilt fast-state still hits this
1866
+ // cache and never schedules a refresh until the TTL itself expires.
1867
+ if (cached && cached.ts && (now - cached.ts) < PROJECT_GIT_STATUS_TTL
1868
+ && !_projectGitRefsAdvancedSince(localPath, cached.ts, configuredMainBranch)) {
1869
+ return cached.value;
1870
+ }
1808
1871
  // Cheap synchronous existsSync — short-circuits a path that just disappeared
1809
1872
  // (project removed) without scheduling a useless git probe.
1810
1873
  if (!fs.existsSync(localPath)) {
@@ -1881,6 +1944,17 @@ function resetProjectGitStatusCache() {
1881
1944
  * directly, and the 10 s TTL covers CLI-driven control-file edits.
1882
1945
  * - `engine/state.json` — surfaced via `getEngineState()` but changes
1883
1946
  * only on engine startup / reconcile. Negligible benefit.
1947
+ * - `engine/metrics.json` — surfaced via `getMetrics()` but written by
1948
+ * `trackEngineUsage()` / `trackAgentMetric()` / the engine tick itself
1949
+ * (~10 s batched flush plus every per-agent metric event). Like
1950
+ * `log.json` and `control.json`, its mtime advances at noise-floor
1951
+ * cadence — every engine tick (~60 s) plus several per-agent writes
1952
+ * in between, so with multiple agents active it bumps every few
1953
+ * seconds and busts the ETag cache on /api/status before steady-state
1954
+ * 304s can engage (W-mphejzct00065d8c). The engine-page metrics tile
1955
+ * is not the hot path and the 10 s `FAST_STATE_TTL` is plenty for
1956
+ * metric freshness; mutating handlers that need immediate metrics
1957
+ * visibility should call `invalidateStatusCache()` directly.
1884
1958
  * - `engine/cooldowns.json`, `engine/pr-links.json`, `engine/pending-
1885
1959
  * rebases.json`, `agents/<id>/managed-spawn.json` — not in the
1886
1960
  * `/api/status` payload.
@@ -1902,11 +1976,15 @@ function resetProjectGitStatusCache() {
1902
1976
  function getStatusFastStateMtimePaths(config) {
1903
1977
  const projects = getProjects(config || getConfig());
1904
1978
  const files = [
1905
- // Engine-level state surfaced by getDispatchQueue / getMetrics.
1906
- // `control.json` and `log.json` are intentionally omitted — see the
1907
- // "Files intentionally NOT tracked" section above (W-mpg8aapw001d7e0c).
1979
+ // Engine-level state surfaced by getDispatchQueue. `control.json`,
1980
+ // `log.json`, and `metrics.json` are intentionally omitted — see the
1981
+ // "Files intentionally NOT tracked" section above
1982
+ // (W-mpg8aapw001d7e0c, W-mphejzct00065d8c). dispatch.json stays
1983
+ // tracked because the home + engine sidebar activity dots and
1984
+ // renderDispatch() consume dispatch transitions at sub-FAST_STATE_TTL
1985
+ // cadence (active → completed flips must light up within one SPA
1986
+ // poll, not wait up to 10 s for the periodic SSE backstop).
1908
1987
  DISPATCH_PATH,
1909
- path.join(ENGINE_DIR, 'metrics.json'),
1910
1988
  // Watches surfaced by watchesMod.getWatches() (W-mpftp7na000td0f4 fix).
1911
1989
  path.join(ENGINE_DIR, 'watches.json'),
1912
1990
  // Central work-items.json surfaced by getWorkItems().
@@ -1955,11 +2033,27 @@ function getStatusFastStateMtimePaths(config) {
1955
2033
  // NOT advanced on a timer — it only moves on user-initiated git
1956
2034
  // operations, so it can't dominate legitimate state changes. Cheap (one
1957
2035
  // statSync per project per cache miss).
2036
+ //
2037
+ // Per-project `.git/FETCH_HEAD` — bare `git fetch` advances FETCH_HEAD
2038
+ // without touching `.git/logs/HEAD` (no HEAD move) or `.git/index` (no
2039
+ // working-tree change). Without this entry, an external `git fetch`
2040
+ // (or another tool background-fetching) leaves the dashboard's
2041
+ // _statusCache and getProjectGitStatus's inner cache both serving the
2042
+ // pre-fetch ahead/behind counts until BOTH the 10s FAST_STATE_TTL and
2043
+ // the 15s probe TTL expire (W-mphdmr8c00030124).
2044
+ //
2045
+ // For linked worktrees (`<localPath>/.git` is a file pointing to
2046
+ // `<main>/.git/worktrees/<name>/`), `_resolveGitDir` walks the pointer
2047
+ // so logs/HEAD and FETCH_HEAD are tracked at the actual gitdir; the
2048
+ // statSync in dashboard's `_getMtimes` tolerates ENOENT, so falling
2049
+ // back to `<localPath>/.git/...` for non-linked-worktree repos is safe.
1958
2050
  for (const p of projects) {
1959
2051
  files.push(shared.projectWorkItemsPath(p));
1960
2052
  files.push(shared.projectPrPath(p));
1961
2053
  if (p && p.localPath) {
1962
- files.push(path.join(p.localPath, '.git', 'logs', 'HEAD'));
2054
+ const gitDir = _resolveGitDir(p.localPath) || path.join(p.localPath, '.git');
2055
+ files.push(path.join(gitDir, 'logs', 'HEAD'));
2056
+ files.push(path.join(gitDir, 'FETCH_HEAD'));
1963
2057
  }
1964
2058
  }
1965
2059
  return files;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2033",
3
+ "version": "0.1.2035",
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"