@yemi33/minions 0.1.2084 → 0.1.2085

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.
@@ -393,8 +393,8 @@ async function openSettings() {
393
393
  '<h4>Inbox &amp; Status Retention</h4>' +
394
394
  '<div class="settings-grid-2">' +
395
395
  settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
396
- settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 7, 'days', 'Trim done/failed/cancelled work items older than N days from the /api/status workItems slice (active items are always shipped). Cuts SPA payload from ~3MB to <500KB. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
397
- settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 7, 'days', 'Trim completed/archived meetings older than N days from the /api/status meetings slice (active meetings are always shipped). Cuts SPA payload from ~4.3MB to <500KB. Detail modal still fetches full transcripts via /api/meetings/:id. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
396
+ settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 0, 'days', 'Optional age-based trim for done/failed/cancelled work items in the /api/status workItems slice (active items are always shipped). Default 0 = no trim — the slim projection that strips description/AC/references already cuts the payload by ~80%. Set to a positive integer to also drop terminal items older than N days.') +
397
+ settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 0, 'days', 'Optional age-based trim for completed/archived meetings in the /api/status meetings slice (active meetings are always shipped). Default 0 = no trim — the slim projection that collapses findings/debate/transcript bodies to {agentId: true} sentinels already cuts the payload by ~80%. Set to a positive integer to also drop terminal meetings older than N days. Detail modal still fetches full bodies via /api/meetings/:id.') +
398
398
  settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
399
399
  '</div>' +
400
400
  '<h4>Operator &amp; Comments</h4>' +
package/dashboard.js CHANGED
@@ -73,6 +73,11 @@ const TITLE_SUFFIX = IS_DEV_MODE ? ' [DEV]' : '';
73
73
 
74
74
  const PORT = parseInt(process.env.PORT || process.argv[2]) || 7331;
75
75
  let CONFIG = queries.getConfig();
76
+ // Mirror cli.js: clear persisted statusWorkItemsRetentionDays=7 + the matching
77
+ // meetings field (prior default 7) so the new default (0, no trim) reaches the
78
+ // /api/status slimmers without needing an engine bounce first.
79
+ try { shared.applyStatusWorkItemsRetentionMigration(CONFIG); } catch { /* best-effort */ }
80
+ try { shared.applyStatusMeetingsRetentionMigration(CONFIG); } catch { /* best-effort */ }
76
81
  let PROJECTS = _getProjects(CONFIG);
77
82
  const CONFIG_PATH = path.join(MINIONS_DIR, 'config.json');
78
83
  const PINNED_PATH = path.join(MINIONS_DIR, 'pinned.md');
@@ -95,6 +100,8 @@ function ensureConfiguredProjectStateFiles() {
95
100
 
96
101
  function reloadConfig() {
97
102
  CONFIG = queries.getConfig();
103
+ try { shared.applyStatusWorkItemsRetentionMigration(CONFIG); } catch { /* best-effort */ }
104
+ try { shared.applyStatusMeetingsRetentionMigration(CONFIG); } catch { /* best-effort */ }
98
105
  PROJECTS = _getProjects(CONFIG);
99
106
  ensureConfiguredProjectStateFiles();
100
107
  }
@@ -1832,19 +1839,19 @@ function _safeStatusSlice(name, fn, fallback) {
1832
1839
  }
1833
1840
 
1834
1841
  // ── /api/status workItems slimming (W-mphejzmj000718bf) ─────────────────────
1835
- // Strip down the workItems slice shipped on /api/status to (a) drop terminal
1836
- // (done/failed/cancelled) items older than engine.statusWorkItemsRetentionDays
1837
- // and (b) project each surviving item onto a narrow shape that omits the
1838
- // large free-text fields (description, full acceptanceCriteria, full
1839
- // references). The dashboard never renders description/AC/references-detail
1840
- // off the cached slice `wiRow` only needs counts + status/badge fields,
1841
- // and `openWorkItemDetail` fetches the full record on demand via
1842
- // GET /api/work-items/<id>. Live measurement at task time: 796 items / ~3MB
1843
- // → ~50–100 items / <500KB typical. Active items (pending/dispatched/queued)
1844
- // are ALWAYS kept regardless of age.
1842
+ // Project each work item onto a narrow shape that omits the large free-text
1843
+ // fields (description, full acceptanceCriteria, full references) before
1844
+ // shipping on /api/status. The dashboard never renders description/AC/
1845
+ // references-detail off the cached slice — `wiRow` only needs counts +
1846
+ // status/badge fields, and `openWorkItemDetail` fetches the full record on
1847
+ // demand via GET /api/work-items/<id>. This slim projection is the bulk of
1848
+ // the payload savings (~3MB <500KB typical) and runs unconditionally.
1845
1849
  //
1846
- // Set engine.statusWorkItemsRetentionDays = 0 to disable trimming entirely
1847
- // (returns the full list unchanged, restoring legacy behavior).
1850
+ // engine.statusWorkItemsRetentionDays is an optional second-tier filter that
1851
+ // drops terminal (done/failed/cancelled) items older than N days. Default 0
1852
+ // = no trim (legacy behavior, full list shipped). Set to a positive integer
1853
+ // to opt into the date-based trim. Active items (pending/dispatched/queued)
1854
+ // are ALWAYS kept regardless of age.
1848
1855
  const _ACTIVE_WI_STATUSES_FOR_STATUS = new Set(['pending', 'dispatched', 'queued', 'paused', 'decomposed']);
1849
1856
  const _TERMINAL_WI_STATUSES_FOR_STATUS = new Set(['done', 'failed', 'cancelled']);
1850
1857
  function _resolveStatusWorkItemsRetentionDays() {
@@ -1931,9 +1938,9 @@ function _slimWorkItemsForStatus(items) {
1931
1938
  }
1932
1939
 
1933
1940
  // ── /api/status meetings slimming (W-mphlrxx6000a8760) ──────────────────────
1934
- // Mirrors the workItems trim above (PR #2816). Meetings are the second
1935
- // largest /api/status slice after workItems — live measurement: 22 meetings
1936
- // / 4.3MB (60% of the 7.2MB payload). The list renderer in
1941
+ // Mirrors the workItems slim above (PR #2816). Meetings were the second
1942
+ // largest /api/status slice — live measurement: 22 meetings / 4.3MB
1943
+ // (60% of the 7.2MB payload) before slimming. The list renderer in
1937
1944
  // dashboard/js/render-meetings.js:renderMeetings only needs:
1938
1945
  // - id, title, status, round, participants, agenda(short), createdAt,
1939
1946
  // completedAt
@@ -1941,13 +1948,13 @@ function _slimWorkItemsForStatus(items) {
1941
1948
  // ✓/⏳/○ icon — `m.findings?.[p]` truthy check, line 48-50)
1942
1949
  // The detail modal calls `/api/meetings/:id` which serves the full record
1943
1950
  // (findings.content + debate.content + conclusion + transcript bodies), so
1944
- // dropping those from the slice is safe.
1951
+ // dropping those bodies from the slice is safe. The slim projection alone
1952
+ // delivers the bulk of the payload savings and always runs.
1945
1953
  //
1946
- // Active meetings (investigating/debating/concluding) are ALWAYS kept
1947
- // regardless of age. Terminal meetings (completed/archived) only survive
1948
- // if their completedAt/roundStartedAt/createdAt is within the window.
1949
- // Set engine.statusMeetingsRetentionDays = 0 to disable trimming entirely
1950
- // (returns the full list — but still slim-shaped — restoring legacy size).
1954
+ // engine.statusMeetingsRetentionDays is an optional second-tier filter that
1955
+ // drops terminal (completed/archived) meetings older than N days. Default 0
1956
+ // = no trim (legacy behavior, full list shipped — but still slim-shaped).
1957
+ // Active meetings (investigating/debating/concluding) are ALWAYS kept.
1951
1958
  const _ACTIVE_MEETING_STATUSES_FOR_STATUS = new Set(['investigating', 'debating', 'concluding']);
1952
1959
  const _TERMINAL_MEETING_STATUSES_FOR_STATUS = new Set(['completed', 'archived']);
1953
1960
  function _resolveStatusMeetingsRetentionDays() {
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "id": "status-retention-stale-default-migration",
4
+ "description": "applyStatusWorkItemsRetentionMigration + applyStatusMeetingsRetentionMigration: one-time migrations that drop engine.statusWorkItemsRetentionDays=7 and engine.statusMeetingsRetentionDays=7 from loaded config so the new defaults (0, no trim) reach installs whose config.json was persisted while 7 was the baked-in default. The 7-day cutoffs were added alongside the slim projections (W-mphejzmj000718bf for workItems, W-mphlrxx6000a8760 for meetings) but surfaced as data loss to users (completed rows disappearing from /api/status after a week). The slim projections (which deliver the bulk of the payload savings) still run unconditionally; only the date filters were demoted to opt-in. shared.js mutates the in-memory config; engine/cli.js follows up with guarded shared.mutateJsonFileLocked calls on config.json so the fields are also removed on disk — affected installs are permanently corrected the first time the engine boots after this ships.",
5
+ "code": [
6
+ { "file": "engine/shared.js", "note": "applyStatusWorkItemsRetentionMigration / applyStatusMeetingsRetentionMigration definitions + _resetStaleRetentionMigrationFlag / _resetStaleMeetingsRetentionMigrationFlag test helpers — pure, in-memory" },
7
+ { "file": "engine/cli.js", "note": "Two boot blocks inside start() — each applies the in-memory pass THEN mutateJsonFileLocked on config.json to delete the field with skipWriteIfUnchanged so non-7 installs don't churn the file" },
8
+ { "file": "dashboard.js", "note": "Initial CONFIG load + reloadConfig() both apply the in-memory migrations so the dashboard picks up the new defaults even if it boots before the engine writes config.json" }
9
+ ],
10
+ "deprecated": "2026-05-29",
11
+ "targetRemovalDate": "2026-06-01",
12
+ "cleanup": "On or after 2026-06-01 (3 days), delete both migration functions + their reset helpers from engine/shared.js (including the shared.js export lines), the boot calls + mutateJsonFileLocked blocks in engine/cli.js, the call sites in dashboard.js, the tests in test/unit/status-workitems-retention.test.js and test/unit/status-meetings-retention.test.js gated on the function names, and this entry. Three days is enough because the on-disk rewrite happens on first engine boot — any install whose engine has rebooted at least once since this shipped has already had its config.json corrected and is no longer dependent on the in-memory shim.",
13
+ "notes": "Persistent custom values (any non-7 integer) are preserved untouched. The only at-risk users are those who explicitly want a 7-day window — they can re-set via the Settings page after removal. If a user never restarts the engine in the 3-day window, the shim still strips the persisted 7 on dashboard-only boot via the in-memory pass; the on-disk write is a hardening pass for the common case, not a strict requirement."
14
+ },
2
15
  {
3
16
  "id": "config-poll-key-migration",
4
17
  "location": "engine/queries.js:126-163",
package/engine/cli.js CHANGED
@@ -471,6 +471,41 @@ const commands = {
471
471
  try { shared.applyLegacyCcModelMigration(config, { logger: e.log }); }
472
472
  catch (err) { e.log('warn', `legacy ccModel migration failed: ${err.message}`); }
473
473
 
474
+ // Drop persisted statusWorkItemsRetentionDays=7 (the prior baked-in default)
475
+ // so the new default of 0 (no trim) reaches installs that opened Settings
476
+ // before the flip. Explicit non-7 values are preserved. We mutate in-memory
477
+ // AND rewrite config.json so the fix is permanent — the shim in shared.js
478
+ // can then retire on schedule without users regressing.
479
+ try {
480
+ const applied = shared.applyStatusWorkItemsRetentionMigration(config, { logger: e.log });
481
+ if (applied) {
482
+ const configPath = path.join(shared.MINIONS_DIR, 'config.json');
483
+ shared.mutateJsonFileLocked(configPath, (onDisk) => {
484
+ if (onDisk && onDisk.engine && onDisk.engine.statusWorkItemsRetentionDays === 7) {
485
+ delete onDisk.engine.statusWorkItemsRetentionDays;
486
+ }
487
+ return onDisk;
488
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
489
+ }
490
+ }
491
+ catch (err) { e.log('warn', `statusWorkItemsRetentionDays migration failed: ${err.message}`); }
492
+
493
+ // Same treatment for statusMeetingsRetentionDays — the meetings slice had
494
+ // the same 7-day baked-in default and the same data-loss UX.
495
+ try {
496
+ const applied = shared.applyStatusMeetingsRetentionMigration(config, { logger: e.log });
497
+ if (applied) {
498
+ const configPath = path.join(shared.MINIONS_DIR, 'config.json');
499
+ shared.mutateJsonFileLocked(configPath, (onDisk) => {
500
+ if (onDisk && onDisk.engine && onDisk.engine.statusMeetingsRetentionDays === 7) {
501
+ delete onDisk.engine.statusMeetingsRetentionDays;
502
+ }
503
+ return onDisk;
504
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
505
+ }
506
+ }
507
+ catch (err) { e.log('warn', `statusMeetingsRetentionDays migration failed: ${err.message}`); }
508
+
474
509
  // Auto-heal projects missing workSources (cloned-repo / hand-rolled-config
475
510
  // footgun): without this block, discoverFromWorkItems / discoverFromPrs
476
511
  // bail silently and the engine looks healthy but never dispatches. The
package/engine/shared.js CHANGED
@@ -2184,27 +2184,32 @@ const ENGINE_DEFAULTS = {
2184
2184
  // the override and falls back to auto-resolution.
2185
2185
  operatorLogin: null,
2186
2186
  // ── /api/status workItems retention (W-mphejzmj000718bf) ────────────────────
2187
- // Trim done/failed/cancelled work items older than this many days from the
2188
- // /api/status workItems slice to cut the SPA payload (live: 796 items / 3MB
2189
- // ~50–100 items / <500KB typical). Active items (pending/dispatched/queued)
2190
- // are ALWAYS shipped regardless of ageonly terminal items past the
2191
- // window are dropped. The detail modal fetches the full record on demand
2192
- // via GET /api/work-items/<id> when description/references/AC are needed.
2193
- // 0 disables the trim (full list shipped, restoring legacy behavior).
2194
- statusWorkItemsRetentionDays: 7,
2187
+ // Optional age-based trim for done/failed/cancelled work items in the
2188
+ // /api/status workItems slice. Default 0 = no trim (full list shipped). The
2189
+ // bulk of the payload savings (~3MB ~500KB) comes from _slimWorkItemForStatus
2190
+ // dropping description / full acceptanceCriteria / references that slim
2191
+ // projection runs unconditionally. The date filter on top was a second-tier
2192
+ // optimization that surfaced as data loss to users (completed items vanishing
2193
+ // from /api/status after 7 days) so it now opts in via a positive integer.
2194
+ // Active items (pending/dispatched/queued) are ALWAYS shipped regardless of
2195
+ // age. The detail modal fetches the full record on demand via
2196
+ // GET /api/work-items/<id> when description/references/AC are needed.
2197
+ statusWorkItemsRetentionDays: 0,
2195
2198
 
2196
2199
  // ── /api/status meetings retention (W-mphlrxx6000a8760) ─────────────────────
2197
- // Same shape as statusWorkItemsRetentionDays — mirrors the trim+slim pass
2198
- // for the meetings slice (live: 22 meetings / 4.3MB ~5 meetings / <500KB
2199
- // typical). Active meetings (investigating/debating/concluding) are ALWAYS
2200
- // shipped regardless of age only terminal meetings (completed/archived)
2201
- // past the window are dropped. The detail modal fetches the full record
2202
- // (findings, debate, conclusion, transcript bodies) on demand via
2203
- // GET /api/meetings/<id> when opened. A top-level meetingsTotal field is
2204
- // synthesized so the sidebar activity dot still fires when ANY meeting
2205
- // (including those dropped from the slim slice) gains a new round.
2206
- // 0 disables the trim (full list shipped, restoring legacy behavior).
2207
- statusMeetingsRetentionDays: 7,
2200
+ // Same shape as statusWorkItemsRetentionDays — optional age-based trim for
2201
+ // completed/archived meetings in the /api/status meetings slice. Default 0
2202
+ // = no trim (full list shipped). The slim projection (which collapses
2203
+ // ~95KB+ per-round findings/debate/transcript bodies down to {agentId: true}
2204
+ // sentinels) delivers the bulk of the payload savings and always runs.
2205
+ // The date filter on top was demoted to opt-in for the same reason as the
2206
+ // workItems trim: vanishing completed meetings read as data loss. Active
2207
+ // meetings (investigating/debating/concluding) are ALWAYS shipped regardless
2208
+ // of age. The detail modal fetches the full record (findings, debate,
2209
+ // conclusion, transcript bodies) on demand via GET /api/meetings/<id>.
2210
+ // A top-level meetingsTotal field is synthesized so the sidebar activity
2211
+ // dot still fires when ANY meeting gains a new round.
2212
+ statusMeetingsRetentionDays: 0,
2208
2213
  };
2209
2214
 
2210
2215
  // ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
@@ -2401,6 +2406,64 @@ function _resetLegacyCcModelMigrationFlag() {
2401
2406
  _legacyCcModelMigrationLogged = false;
2402
2407
  }
2403
2408
 
2409
+ // ─── Stale statusWorkItemsRetentionDays Default Migration ────────────────────
2410
+ //
2411
+ // The retention default was 7 from W-mphejzmj000718bf until users reported the
2412
+ // trim hid completed work items from /api/status, which read as data loss.
2413
+ // We flipped the baked-in default to 0 (no trim). Installs that opened the
2414
+ // Settings page while the default was 7 have `engine.statusWorkItemsRetentionDays: 7`
2415
+ // persisted in their config.json — the resolver would return 7 and they'd
2416
+ // still see the trim. This shim drops a literal `7` at load time so the new
2417
+ // default of 0 applies. Operators who explicitly set a non-7 value (e.g. 14
2418
+ // or 30) are left untouched. No on-disk rewrite.
2419
+
2420
+ let _staleRetentionMigrationLogged = false;
2421
+
2422
+ function applyStatusWorkItemsRetentionMigration(config, { logger = log } = {}) {
2423
+ if (!config || !config.engine || typeof config.engine !== 'object') return false;
2424
+ const e = config.engine;
2425
+ if (e.statusWorkItemsRetentionDays !== 7) return false;
2426
+ delete e.statusWorkItemsRetentionDays;
2427
+ if (!_staleRetentionMigrationLogged) {
2428
+ _staleRetentionMigrationLogged = true;
2429
+ try {
2430
+ logger('warn', 'statusWorkItemsRetentionDays=7 was the previous default — clearing in-memory so the new default (0, no trim) applies. Re-save Settings to persist or set a positive value to opt back in.');
2431
+ } catch { /* logger may not be wired during tests — best-effort */ }
2432
+ }
2433
+ return true;
2434
+ }
2435
+
2436
+ /** Test helper: reset the dedup flag so repeated tests can re-trigger the log. */
2437
+ function _resetStaleRetentionMigrationFlag() {
2438
+ _staleRetentionMigrationLogged = false;
2439
+ }
2440
+
2441
+ // Same shape as applyStatusWorkItemsRetentionMigration above, for the meetings
2442
+ // slice. The prior baked-in default of 7 caused completed/archived meetings to
2443
+ // vanish from /api/status after a week; we flipped the default to 0 and strip
2444
+ // the literal 7 from persisted configs so the new behavior applies.
2445
+
2446
+ let _staleMeetingsRetentionMigrationLogged = false;
2447
+
2448
+ function applyStatusMeetingsRetentionMigration(config, { logger = log } = {}) {
2449
+ if (!config || !config.engine || typeof config.engine !== 'object') return false;
2450
+ const e = config.engine;
2451
+ if (e.statusMeetingsRetentionDays !== 7) return false;
2452
+ delete e.statusMeetingsRetentionDays;
2453
+ if (!_staleMeetingsRetentionMigrationLogged) {
2454
+ _staleMeetingsRetentionMigrationLogged = true;
2455
+ try {
2456
+ logger('warn', 'statusMeetingsRetentionDays=7 was the previous default — clearing in-memory so the new default (0, no trim) applies. Re-save Settings to persist or set a positive value to opt back in.');
2457
+ } catch { /* logger may not be wired during tests — best-effort */ }
2458
+ }
2459
+ return true;
2460
+ }
2461
+
2462
+ /** Test helper: reset the dedup flag so repeated tests can re-trigger the log. */
2463
+ function _resetStaleMeetingsRetentionMigrationFlag() {
2464
+ _staleMeetingsRetentionMigrationLogged = false;
2465
+ }
2466
+
2404
2467
  // ─── Runtime Config Preflight Warnings ──────────────────────────────────────
2405
2468
  //
2406
2469
  // Emit non-fatal warnings about runtime/CLI configuration drift. Consumed by
@@ -5454,6 +5517,8 @@ module.exports = {
5454
5517
  resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
5455
5518
  resolveAgentMaxBudget, resolveAgentBareMode,
5456
5519
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
5520
+ applyStatusWorkItemsRetentionMigration, _resetStaleRetentionMigrationFlag,
5521
+ applyStatusMeetingsRetentionMigration, _resetStaleMeetingsRetentionMigrationFlag,
5457
5522
  runtimeConfigWarnings,
5458
5523
  projectWorkSourceWarnings,
5459
5524
  backfillProjectWorkSourceDefaults,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2084",
3
+ "version": "0.1.2085",
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"