@yemi33/minions 0.1.2451 → 0.1.2452
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/docs/README.md
CHANGED
|
@@ -44,6 +44,7 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
44
44
|
- [cooldown-merge-semantics.md](cooldown-merge-semantics.md) — Scoping deliverable defining merge semantics for `saveCooldowns` (longer-of TTL merge, key-level upserts, gitignored on-disk format).
|
|
45
45
|
- [copilot-cli-schema.md](copilot-cli-schema.md) — Behavior and schema reference for the GitHub Copilot CLI adapter (capability flags, stdin vs `-p`, model discovery, effort levels).
|
|
46
46
|
- [cross-repo-plans.md](cross-repo-plans.md) — Cross-repo plans: a single plan whose work items ship into two or more configured projects — per-item `project` field, per-project work-item fan-out, and one verify work item per touched repo.
|
|
47
|
+
- [dashboard-reliability-and-responsiveness.md](dashboard-reliability-and-responsiveness.md) — Reliability/perf deep dive on what starves the single dashboard event loop: the `GET /api/work-items` full-enrichment stall (measured ~3.3 s p50 at 2,300 items; production 7–17 s), the ETag/`MAX(events.id)` re-run trigger, the health-probe/CC-SSE false-disconnect chain, a threading/process-boundary decision matrix, a P0/P1/P2 roadmap, and explicit worker-thread non-recommendations. Distinguishes merged safeguards from the still-in-flight fix (PR #1198).
|
|
47
48
|
- [dead-code-audit-retractions.md](dead-code-audit-retractions.md) — Retracted dead-code-audit findings (false positives) that future audits MUST read before re-citing.
|
|
48
49
|
- [default-branch-ci.md](default-branch-ci.md) — The post-merge default-branch gate: why a `pull_request` gate cannot catch merge-order defects (two green PRs that break only in combination), why the post-merge workflow *calls* the PR gate rather than copying it or inverting the direction (the ruleset requires the `Unit Tests` context by exact string), the cost controls, and the evaluated-but-rejected merge-queue alternative.
|
|
49
50
|
- [deprecated-process.md](deprecated-process.md) — Schema for `docs/deprecated.json` and the weekly `cleanup-deprecated` audit walk that retires entries past their removal signal.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Dashboard Reliability & Responsiveness
|
|
2
|
+
|
|
3
|
+
Deep dive into what keeps (and what starves) the Minions dashboard/API event loop
|
|
4
|
+
under realistic engine + UI load. Verified against `engine/core/queries.js`,
|
|
5
|
+
`dashboard.js`, and `engine/observability/diagnostics-memory.js` on `work/W-msci5nby0044fd1f`
|
|
6
|
+
(branched from `main`), with reproducible measurements from an isolated benchmark
|
|
7
|
+
(see [Appendix A](#appendix-a--reproduction)).
|
|
8
|
+
|
|
9
|
+
**Audience:** engine/dashboard contributors filing follow-up performance work. This is
|
|
10
|
+
a decision document — each finding names the module boundary that changes, the fix
|
|
11
|
+
class, and an acceptance metric. It is **not** an implementation.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Executive summary
|
|
16
|
+
|
|
17
|
+
The dashboard is a single Node process (`dashboard.js`, default port 7331) sharing one
|
|
18
|
+
event loop across: the 4 s SPA poll (`/api/status` + per-page list endpoints), the
|
|
19
|
+
250 ms-cadence `/api/health` restart probe, Command Center / doc-chat SSE streams, and
|
|
20
|
+
all mutating API calls. Any synchronous region longer than a few hundred milliseconds on
|
|
21
|
+
that loop degrades *every* concurrent consumer at once.
|
|
22
|
+
|
|
23
|
+
**The dominant offender is `GET /api/work-items`.** Its list builder runs
|
|
24
|
+
`queries.getWorkItems()` with **full enrichment** (`enrich: true`), whose
|
|
25
|
+
`_artifacts`/`_notes` step does per-item bucketed scans over the knowledge-base snapshot,
|
|
26
|
+
the notes archive, per-agent output dirs, and the inbox — `O(items × files)` synchronous
|
|
27
|
+
work. On a history of ~2,300 work items with a comparably sized notes archive, a single
|
|
28
|
+
cold/cache-busted enrichment blocks the event loop for **~3.3 s p50 / ~3.9 s p95**
|
|
29
|
+
(measured; [Appendix A](#appendix-a--reproduction)); production incidents at ~2,281 items
|
|
30
|
+
reported **7–17 s** (larger KB, colder OS file cache, slower disk).
|
|
31
|
+
|
|
32
|
+
**User-visible failure chain:**
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
engine tick writes state (events.id bumps every tick)
|
|
36
|
+
→ /api/work-items ETag busts → list builder runs full enrichment every ~4s poll
|
|
37
|
+
→ synchronous O(items×files) scan blocks the single event loop for seconds
|
|
38
|
+
→ /api/health 250ms probe misses → supervisor sees a "dead" dashboard
|
|
39
|
+
→ CC SSE 15s heartbeat fires late → [cc-stall] logged, browser reconnects
|
|
40
|
+
→ browser fetch() times out → "TypeError: Failed to fetch" false-disconnect UI
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Status of the fix.** The root-cause fix — serving the list from the *lean*
|
|
44
|
+
(`enrich: false`) path — is **in flight, not merged**: PR **#1198** ("Make work-items
|
|
45
|
+
list event-loop safe") rewrites the route builder to `getWorkItems(enrich:false)` +
|
|
46
|
+
pagination + a cheap map-based PR link-up, leaving detail-only fields on
|
|
47
|
+
`GET /api/work-items/<id>`. On `main` today the route still calls the enriching
|
|
48
|
+
`getWorkItems().map(slimWorkItemForList)` (`dashboard.js`, `/api/work-items` GET route),
|
|
49
|
+
so the starvation path is **still present until #1198 lands**. An earlier merged fix
|
|
50
|
+
(PR **#456**) reduced the same enrichment's complexity with per-agent bucketing but did
|
|
51
|
+
not take the route off the enrich path. See [§7](#7-already-fixed-vs-still-outstanding).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 2. Current architecture (responsiveness-relevant slice)
|
|
56
|
+
|
|
57
|
+
| Surface | Where | Freshness / isolation today |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `/api/status` | `handleStatus` → `refreshStatusAsync` (`dashboard.js`) | Single-flight `_statusRebuildPromise`; ETag on `_statusCacheVersion`; one cooperative `await _yieldEventLoop()` between fast/slow halves; invalidation-race guard. **Well isolated.** |
|
|
60
|
+
| `/api/work-items` (list) | `/api/work-items` GET builder → `queries.getWorkItems()` | `serveFreshJson`, ETag = `tag-variant-mtime-eventVersion`. `eventVersion` = `SELECT MAX(id) FROM events` (`_getCurrentEventVersion`), so on a busy engine the ETag busts **every poll** and the builder re-runs. Builder is **synchronous** with no yield. |
|
|
61
|
+
| `/api/work-items/<id>` (detail) | `handleWorkItemsById` | Full single-record read; backs the detail/edit modal. |
|
|
62
|
+
| `/api/health` | `handleHealth` | Minimal inline agent array from config + dispatch only (two cached reads, no dir enumeration) — deliberately kept off `getAgents()`/`/api/status`. **Well isolated.** |
|
|
63
|
+
| CC / doc-chat SSE | `ccCallStreaming`, doc-chat handlers | 15 s heartbeat (`CC_STREAM_HEARTBEAT_MS`); `[cc-stall]` logged when it fires > 3 s late (`CC_STREAM_STALL_THRESHOLD_MS`); per-tab queue shed at 4 MB / 30 s backpressure (`SSE_MAX_QUEUE_BYTES`, `SSE_STUCK_KILL_MS`). Heartbeat is a timer → **starved by any long sync region**. |
|
|
64
|
+
| Memory / event-loop-lag observability | `engine/observability/diagnostics-memory.js` | Engine samples every `memoryBaselineEveryTicks` (≈60 s). Dashboard samples via `startPeriodicSampling({ intervalMs: 60000 })`. Event-loop-lag histogram exists but is **sampled only once per 60 s** on the dashboard side. |
|
|
65
|
+
|
|
66
|
+
### The read-side aggregation layer
|
|
67
|
+
|
|
68
|
+
`queries.getWorkItems(config, { enrich })` (`engine/core/queries.js`) is the hinge:
|
|
69
|
+
|
|
70
|
+
- **`enrich: false` (lean):** SQL rows (`work-items-store.readAllWorkItems()`) + dispatch
|
|
71
|
+
cross-reference + pending-gate annotation + the deterministic sort. **No filesystem
|
|
72
|
+
scans.** 1 s TTL cache (`_workItemsLeanCache`).
|
|
73
|
+
- **`enrich: true` (default):** everything above **plus** PR cross-reference and the
|
|
74
|
+
per-item `_artifacts`/`_notes`/`_model` detail-modal fields. This is where the
|
|
75
|
+
`O(items × files)` cost lives. Its own header comment documents the freeze and the
|
|
76
|
+
per-agent bucketing that PR #456 added; a `_buildNotesByWiMap` + mtime-gated archive
|
|
77
|
+
cache (`_buildArchiveNoteRefs`, 5 min TTL) further amortize the archive read — **but any
|
|
78
|
+
archive add/remove busts that cache**, and consolidation moves inbox → archive
|
|
79
|
+
routinely, so the "warm" state is not durable under a live engine.
|
|
80
|
+
|
|
81
|
+
Both share a 1 s TTL so intra-request duplication (`getWorkItems` is called 5–7× per
|
|
82
|
+
`/api/status` rebuild) collapses to one scan; the TTL does not help *across* poll cycles
|
|
83
|
+
once `eventVersion` busts the route ETag.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. Measured bottlenecks (evidence table)
|
|
88
|
+
|
|
89
|
+
Confidence: **H** = directly measured or unambiguous in code; **M** = strong code
|
|
90
|
+
evidence + incident correlation; **L** = plausible, needs instrumentation to confirm.
|
|
91
|
+
|
|
92
|
+
| # | Finding | Severity | Conf. | Reproduction | Affected surfaces |
|
|
93
|
+
|---|---|---|---|---|---|
|
|
94
|
+
| E1 | `/api/work-items` list runs full `enrich:true` enrichment; a cold/cache-busted call blocks the loop ~3.3 s p50 / ~3.9 s p95 at 2,300 items (production 7–17 s) | **P0** | H | [Appendix A](#appendix-a--reproduction) | health probe, CC SSE, every concurrent fetch |
|
|
95
|
+
| E2 | Route ETag includes `MAX(events.id)`; a busy engine bumps it every tick, so the enrich builder re-runs on essentially every 4 s poll instead of 304-ing | **P0** | H | `serveFreshJson` ETag = `…-mtime-eventVersion`; `_getCurrentEventVersion` | list latency, loop fairness |
|
|
96
|
+
| E3 | The enrich builder is one uninterrupted synchronous region — no `await _yieldEventLoop()` mid-scan (unlike `/api/status`) | **P0** | H | `getWorkItems` enrich block is straight-line sync | loop fairness |
|
|
97
|
+
| E4 | Archive notes-by-WI cache is busted by any archive add/remove; consolidation writes archive notes under a live engine, so the multi-second cold scan recurs | **P1** | H | Bench with per-iteration archive write: p50 rebuild 3.3 s vs 63 ms when cache holds | list latency spikes |
|
|
98
|
+
| E5 | KB snapshot bucketing scales with KB size (production KB ≈ 4,600 indexed + up to ~10k entries); benchmark understates it (cold snapshot returns `[]`) | **P1** | M | `getKnowledgeBaseEntriesSnapshot()` async warm; `_kbByAgent` bucketing in `getWorkItems` | list latency at scale |
|
|
99
|
+
| E6 | Dashboard event-loop-lag is sampled once per 60 s; a 3–17 s spike is usually invisible to `/api/diagnostics/memory` | **P1** | H | `DIAGNOSTICS_MEMORY_SAMPLE_INTERVAL_MS = 60000` | observability blind spot |
|
|
100
|
+
| E7 | No per-route duration / event-loop-delay instrumentation on the dashboard; `[cc-stall]` is the only starvation signal and it is CC-specific | **P1** | H | grep: no `monitorEventLoopDelay` in `dashboard.js` request path | triage blind spot |
|
|
101
|
+
| E8 | `_getCurrentEventVersion()` runs a synchronous `prepare().get()` against SQLite on every `serveFreshJson` request (cheap, but on the hot loop) | **P2** | M | `_getCurrentEventVersion` | micro-overhead |
|
|
102
|
+
| E9 | Supervisor treats a starved-but-alive dashboard as dead → false restart, which drops in-flight SSE and compounds the perceived outage | **P1** | M | `/api/health` polled every 250 ms for 15 s post-restart (`handleHealth` header) | restart churn |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 4. Threading / process-boundary decision matrix
|
|
107
|
+
|
|
108
|
+
The primary question is not "which threads help?" but "which synchronous work should not
|
|
109
|
+
exist on the hot loop at all?". Prefer, in order: **remove → cache/index → chunk/yield →
|
|
110
|
+
worker thread → child process.**
|
|
111
|
+
|
|
112
|
+
| Work | Keep on loop | Chunk/yield | Cache/index/query | Worker thread | Child process | Chosen | Why |
|
|
113
|
+
|---|:--:|:--:|:--:|:--:|:--:|---|---|
|
|
114
|
+
| Work-items **list** payload | | | ✅ | | | **Serve lean (E1/E2)** | Remove the fs scan from the list path entirely (PR #1198). No concurrency needed — the data is already in SQL. |
|
|
115
|
+
| Per-item `_artifacts`/`_notes` enrichment | | ✅ | ✅ | | | **Detail-only + index** | Only the detail modal needs it; compute per-id on `/<id>`, or precompute a WI→artifacts index in SQL. |
|
|
116
|
+
| `/api/status` rebuild | | ✅ | ✅ | | | **Already done** | Single-flight + mid-rebuild yield already isolate it. |
|
|
117
|
+
| KB / notes text search for linkage | | | ✅ | ⚠️ | | **Index in SQL/FTS5** | FTS5 already backs memory retrieval; reuse it instead of `String.includes` scans. A worker still pays transfer cost for a scan that an index removes. |
|
|
118
|
+
| Health probe | ✅ | | | | | **Already minimal** | Must never touch heavy reads. |
|
|
119
|
+
| CC / doc-chat LLM turns | | | | | ✅ | **Already isolated** | Spawned children; only their SSE writer touches the loop. |
|
|
120
|
+
| Skills / tools / MCP discovery | | | ✅ | | ✅ | **Already child-process** | `/api/tools` discovers in a child; keep it off `/api/status`. |
|
|
121
|
+
|
|
122
|
+
**Guiding rule:** if an index or a cache removes the cost, a worker thread is the *wrong*
|
|
123
|
+
tool — it adds serialization + transfer overhead to hide work that should not run. Worker
|
|
124
|
+
threads earn their keep only for genuinely CPU-bound, non-SQL, non-fs compute that cannot
|
|
125
|
+
be indexed away (none identified today).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 5. Prioritized roadmap
|
|
130
|
+
|
|
131
|
+
### P0 — take the list off the enrich path (removes the starvation)
|
|
132
|
+
|
|
133
|
+
- **P0-1 — Land PR #1198 (list serves `enrich:false`).**
|
|
134
|
+
- **Modules:** `dashboard.js` `/api/work-items` GET builder; `queries.attachWorkItemPrLinks` (new, `engine/core/queries.js`); `dashboard/js/render-work-items.js` (detail hydration).
|
|
135
|
+
- **Success metric:** with an engine actively writing state, `/api/health` p99 stays < 500 ms and no `[cc-stall]` fires while `/api/work-items` is polled at 4 s over a 2,300-item history; list rebuild p95 < 50 ms (from ~3.9 s).
|
|
136
|
+
- **Rollback:** the change is builder-local and byte-compatible on the no-paging path; revert the single route builder to restore prior behavior.
|
|
137
|
+
- **P0-2 — Regression guard.** Add/keep a source-and-timing test asserting the list route
|
|
138
|
+
never calls `getWorkItems(enrich:true)` and that a synthetic 2k-item rebuild stays under
|
|
139
|
+
a budget (the `test/unit/dashboard-work-items-list-event-loop-safe.test.js` added by
|
|
140
|
+
#1198 is the anchor).
|
|
141
|
+
- **Success metric:** test fails if any future edit reintroduces enrich-on-list.
|
|
142
|
+
|
|
143
|
+
### P1 — establish performance budgets & observability *before* adding concurrency
|
|
144
|
+
|
|
145
|
+
- **P1-1 — Dashboard request-path event-loop-delay instrumentation.**
|
|
146
|
+
- **Modules:** `dashboard.js` request wrapper; reuse `engine/observability/diagnostics-memory.js` histogram.
|
|
147
|
+
- **Deliverable:** a bounded per-route duration + a continuously-enabled
|
|
148
|
+
`monitorEventLoopDelay` histogram surfaced on `/api/diagnostics/memory` (not just a
|
|
149
|
+
60 s point sample), plus a `[slow-route]` warn log over a configurable threshold.
|
|
150
|
+
- **Success metric:** a synthetic 1 s block is visible in `/api/diagnostics/memory`
|
|
151
|
+
within one poll interval; today it is usually invisible (E6/E7).
|
|
152
|
+
- **P1-2 — Precompute the WI→artifacts/notes index in SQL.**
|
|
153
|
+
- **Modules:** a numbered migration + store helper; `queries.getWorkItems` enrich block
|
|
154
|
+
reads the index instead of scanning KB snapshot + archive + agent dirs.
|
|
155
|
+
- **Success metric:** detail enrichment for one item is O(1) index lookups, no directory
|
|
156
|
+
walk; the archive-cache-bust cliff (E4) disappears.
|
|
157
|
+
- **P1-3 — Supervisor false-positive hardening.** Confirm the restart probe distinguishes
|
|
158
|
+
"dashboard process alive, status cache slow" (already exposed via
|
|
159
|
+
`handleHealth`'s `dashboardStartedAt`) from "new process". Ensure a single slow
|
|
160
|
+
`/api/status` cannot trip a restart when `/api/health` is answering.
|
|
161
|
+
- **Success metric:** no supervisor restart is triggered while `/api/health` p99
|
|
162
|
+
< 500 ms, even if `/api/status` is momentarily stale.
|
|
163
|
+
|
|
164
|
+
### P2 — micro-overheads & scale hygiene
|
|
165
|
+
|
|
166
|
+
- **P2-1 — Cache `_getCurrentEventVersion()` for a sub-second TTL** so the per-request
|
|
167
|
+
SQLite `MAX(id)` read (E8) is not repeated across concurrent polls in the same tick.
|
|
168
|
+
- **P2-2 — Pagination default-on for the SPA list poll.** The `_parsePageParams` /
|
|
169
|
+
`_paginateList` foundation and `engine/api-contracts/paging.js` bounds already exist;
|
|
170
|
+
have the classic 4 s loop request a bounded first page instead of the full array.
|
|
171
|
+
- **P2-3 — Database-growth soak.** Extend `test/perf/soak.test.js` with a work-items /
|
|
172
|
+
archive growth axis so latency regressions at 5k–10k items are caught pre-merge.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 6. Proposed follow-up work items (independently shippable)
|
|
177
|
+
|
|
178
|
+
Each is small enough to file directly as a WI. All are documentation-derived; none bundle
|
|
179
|
+
a speculative production refactor.
|
|
180
|
+
|
|
181
|
+
1. **Merge/rebase PR #1198** and confirm P0-1 acceptance metric on a live-ish dataset
|
|
182
|
+
(regression test + manual `/api/health` latency check). *(depends: nothing)*
|
|
183
|
+
2. **Dashboard event-loop-delay + per-route timing** (P1-1). Ships a diagnostic endpoint
|
|
184
|
+
surface + `[slow-route]` log; observability-only, no behavior change.
|
|
185
|
+
*(depends: nothing; unblocks measuring everything below)*
|
|
186
|
+
3. **SQL WI→artifacts/notes index** (P1-2). Migration + store helper + `getWorkItems`
|
|
187
|
+
enrich read-path swap; keep the fs-scan fallback for one release behind the index.
|
|
188
|
+
*(depends: #2 for before/after numbers)*
|
|
189
|
+
4. **Event-version read cache** (P2-1). One-line-scope TTL around `_getCurrentEventVersion`.
|
|
190
|
+
*(depends: nothing)*
|
|
191
|
+
5. **SPA list poll → paginated** (P2-2). Frontend + route wiring only.
|
|
192
|
+
*(depends: #1 so the page path is already lean)*
|
|
193
|
+
6. **Work-items growth soak axis** (P2-3). Test-only.
|
|
194
|
+
*(depends: #2 for the budget threshold)*
|
|
195
|
+
|
|
196
|
+
Every item above ships with (a) a regression test or benchmark, and (b) an observability
|
|
197
|
+
requirement satisfied by #2 (a starvation must be *visible* before and *absent* after).
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 7. Already-fixed vs still-outstanding
|
|
202
|
+
|
|
203
|
+
**Merged / present on `main` (do not re-file):**
|
|
204
|
+
|
|
205
|
+
- `/api/status` event-loop isolation: single-flight `_statusRebuildPromise`, ETag on
|
|
206
|
+
`_statusCacheVersion`, mid-rebuild `await _yieldEventLoop()`, invalidation-race guard.
|
|
207
|
+
- `getWorkItems` 1 s TTL caches (enriched + lean, held separately) and per-agent bucketing
|
|
208
|
+
of the KB/archive scans (PR #456) — reduced the constant factor.
|
|
209
|
+
- `slimWorkItemForList`: hard-caps `description` to 2,048 chars and replaces
|
|
210
|
+
`acceptanceCriteria`/`references` arrays with `*Count` integers, so a single oversized
|
|
211
|
+
transcript can no longer balloon the list payload (bounds bytes, not compute).
|
|
212
|
+
- `/api/health` minimal inline agents (no directory enumeration) — keeps the restart probe
|
|
213
|
+
cheap.
|
|
214
|
+
- CC SSE backpressure shedding (`SSE_MAX_QUEUE_BYTES` / `SSE_STUCK_KILL_MS`) and
|
|
215
|
+
`[cc-stall]` / `[cc-sse-backpressure]` logging.
|
|
216
|
+
- List pagination *foundation* (`_parsePageParams` / `_paginateList`, bounds from
|
|
217
|
+
`engine/api-contracts/paging.js`) — present but not yet the default for the SPA poll.
|
|
218
|
+
|
|
219
|
+
**Still outstanding:**
|
|
220
|
+
|
|
221
|
+
- **P0:** the list route still enriches on `main` (fixed only once PR #1198 merges) — E1/E2/E3.
|
|
222
|
+
- **P1:** archive-cache-bust cliff (E4), KB-scale enrichment cost (E5), coarse dashboard
|
|
223
|
+
lag sampling (E6), no per-route timing (E7), supervisor false-positive hardening (E9).
|
|
224
|
+
- **P2:** per-request `MAX(id)` read (E8), pagination-by-default, growth soak.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 8. Non-recommendations & traps
|
|
229
|
+
|
|
230
|
+
- **Do not move `getWorkItems` enrichment onto a worker thread.** The cost is fs + SQL
|
|
231
|
+
scanning, not CPU math. `node:sqlite` statement objects are **not transferable** across
|
|
232
|
+
worker boundaries, and structured-cloning a 2k-item enriched list back to the main
|
|
233
|
+
thread would re-introduce a serialization stall. Remove the scan (index it) instead.
|
|
234
|
+
- **Do not wrap cheap operations in workers.** Worker spawn + message serialization dwarfs
|
|
235
|
+
a sub-millisecond SQL read; the lean list path is already ~11–20 ms.
|
|
236
|
+
- **Do not "fix" starvation by raising health/probe timeouts.** That hides the loop block
|
|
237
|
+
from the supervisor while browsers still see `Failed to fetch`. Extending
|
|
238
|
+
`restartHealthTimeoutMs` treats the symptom; removing the sync region treats the cause.
|
|
239
|
+
- **Do not add a second always-on cache without an invalidation source.** The archive
|
|
240
|
+
cache already shows how a cache that a live engine busts is worse than no cache (a
|
|
241
|
+
surprise multi-second cliff). Prefer an index keyed by the same events the ETag reads.
|
|
242
|
+
- **Do not push `LIMIT/OFFSET` into the per-scope stores for the list** without accounting
|
|
243
|
+
for the post-enrichment cross-scope sort — the current design slices the deterministic
|
|
244
|
+
cached array at the endpoint precisely so `total` matches `/api/status` counts and pages
|
|
245
|
+
never overlap (see the pagination design note in `dashboard.js`).
|
|
246
|
+
- **Do not conflate `/api/status` with the list endpoints.** `/api/status` is already
|
|
247
|
+
isolated; the regression risk is re-coupling a heavy list build back into the status
|
|
248
|
+
rebuild.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Appendix A — Reproduction
|
|
253
|
+
|
|
254
|
+
Method: an isolated `MINIONS_TEST_DIR` (via the test harness `createTestMinionsDir()`)
|
|
255
|
+
seeded with **2,300 work items**, **2,300 archive notes**, and 5 agents × 40 output logs,
|
|
256
|
+
exercising the **real** `engine/core/queries.getWorkItems`. Event-loop blocking equals the
|
|
257
|
+
synchronous wall-time of the call (the builder has no `await`), so a cold call of duration
|
|
258
|
+
_D_ blocks the loop for _D_. The archive notes-by-WI cache was busted each iteration (one
|
|
259
|
+
archive write) to model consolidation writing under a live engine.
|
|
260
|
+
|
|
261
|
+
Machine/runtime: Node v24.14.0, win32, warm SSD. Numbers are order-of-magnitude, not a
|
|
262
|
+
guaranteed SLA — production disks and a populated KB snapshot make the enrich path slower,
|
|
263
|
+
consistent with the 7–17 s incident reports.
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
dataset: 2300 work items, 2300 archive notes, 5 agents
|
|
267
|
+
enrich:true COLD = ~3.2–3.8 s (2300 items) ← single event-loop block
|
|
268
|
+
enrich:false COLD = ~15–20 ms (2300 items)
|
|
269
|
+
enrich:true rebuild p50=3264ms p95=3942ms (archive cache busted each poll)
|
|
270
|
+
enrich:false rebuild p50=11ms p95=13ms
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Ratio: the lean path is **~250–300× cheaper** and, crucially, does **zero** filesystem
|
|
274
|
+
scanning, so it cannot be pushed back onto the multi-second cliff by a KB/archive write.
|
|
275
|
+
|
|
276
|
+
The benchmark script is intentionally **not committed** — it is a throwaway diagnostic. To
|
|
277
|
+
reproduce, adapt the harness in `test/unit/queries-work-items-cache.test.js`
|
|
278
|
+
(`createTestMinionsDir` + `seedWorkItems`), seed a large archive under
|
|
279
|
+
`notes/archive/`, and time `queries.getWorkItems(null, { enrich })` both ways. The
|
|
280
|
+
regression anchor that *is* committed (once PR #1198 lands) is
|
|
281
|
+
`test/unit/dashboard-work-items-list-event-loop-safe.test.js`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2452",
|
|
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"
|