@yemi33/minions 0.1.2453 → 0.1.2455
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/bin/install-internal-minions.js +136 -6
- package/bin/minions.js +27 -13
- package/dashboard/js/agent-identity.js +121 -0
- package/dashboard/js/modal.js +4 -0
- package/dashboard/js/refresh.js +38 -3
- package/dashboard/js/render-agents.js +14 -2
- package/dashboard/js/render-dispatch.js +2 -2
- package/dashboard/js/render-other.js +147 -13
- package/dashboard/js/render-prd.js +6 -5
- package/dashboard/js/render-prs.js +44 -25
- package/dashboard/js/render-work-items.js +442 -22
- package/dashboard/js/settings.js +8 -0
- package/dashboard/js/utils.js +40 -0
- package/dashboard/pages/tools.html +1 -0
- package/dashboard/pages/work.html +13 -0
- package/dashboard/shared/pr-author.js +75 -0
- package/dashboard/shared/pr-filters.js +28 -5
- package/dashboard/styles.css +85 -0
- package/dashboard-build.js +3 -3
- package/dashboard.js +93 -7
- package/docs/README.md +1 -0
- package/docs/copilot-cli-schema.md +1 -0
- package/docs/engine-restart.md +4 -2
- package/docs/internal-install.md +44 -5
- package/docs/named-agents.md +48 -0
- package/docs/pr-author-identity.md +63 -10
- package/docs/runtime-adapters.md +39 -0
- package/docs/temporary-agents.md +172 -0
- package/engine/ado/comment.js +261 -4
- package/engine/agents/llm.js +26 -0
- package/engine/agents/playbook.js +2 -1
- package/engine/api/settings-validation.js +25 -0
- package/engine/core/operator-identity.js +23 -1
- package/engine/core/queries.js +53 -2
- package/engine/core/shared.js +286 -9
- package/engine/db/migrations/032-review-enrolled-pr-context-only.js +95 -0
- package/engine/operations/cli.js +102 -1
- package/engine/orchestration/lifecycle.js +7 -0
- package/engine/orchestration/routing.js +4 -1
- package/engine/providers/gh-comment.js +159 -0
- package/engine/recovery/stop-stack.js +16 -2
- package/engine/runtimes/claude.js +3 -0
- package/engine/runtimes/codex.js +4 -0
- package/engine/runtimes/copilot.js +23 -2
- package/engine.js +2 -2
- package/package.json +1 -1
- package/playbooks/fix.md +23 -2
- package/playbooks/shared-rules.md +24 -2
|
@@ -1143,7 +1143,47 @@ const BACKUP_COPIED_FILES = ['config.json', 'routing.md', 'pinned.md'];
|
|
|
1143
1143
|
/** Subdirectory of the backup holding the ORIGINAL, byte-for-byte state files. */
|
|
1144
1144
|
const RETAINED_ORIGINAL_DIR = 'original';
|
|
1145
1145
|
|
|
1146
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* The FLOOR of the wait budget, and the fixed window this gate used to have.
|
|
1148
|
+
*
|
|
1149
|
+
* The budget is now derived from the runtime being stopped (see
|
|
1150
|
+
* `resolveQuiesceTimeoutMs`), but it may only ever WIDEN from here: a runtime
|
|
1151
|
+
* configured with a very short drain must not make the migration gate stricter
|
|
1152
|
+
* than it has always been. It is also `stop-stack.js#DEFAULT_STOP_TIMEOUT_MS`,
|
|
1153
|
+
* so the wait after a stop is never shorter than the stop's own budget.
|
|
1154
|
+
*/
|
|
1155
|
+
const QUIESCE_TIMEOUT_FLOOR_MS = 60000;
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* The absolute CEILING on the wait budget.
|
|
1159
|
+
*
|
|
1160
|
+
* The derived budget is operator-supplied data read from a file this gate does
|
|
1161
|
+
* not own, so it is bounded: a corrupt or hostile `config.json` must not be able
|
|
1162
|
+
* to hang a migration indefinitely on an unattended machine. 15 minutes is ~3x
|
|
1163
|
+
* the default runtime's own budget, so it clamps nothing a real stack needs.
|
|
1164
|
+
*/
|
|
1165
|
+
const QUIESCE_TIMEOUT_CEILING_MS = 900000;
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Grace added on top of the engine's own drain budget, mirroring
|
|
1169
|
+
* `engine/recovery/stop-stack.js#DRAIN_GRACE_MS` — the SAME number `minions
|
|
1170
|
+
* restart` applies when it derives a teardown deadline the same way. It covers
|
|
1171
|
+
* the fixed work either side of the drain: stop intent, the supervisor and
|
|
1172
|
+
* dashboard, the verified reap, and the WAL-index release.
|
|
1173
|
+
*/
|
|
1174
|
+
const QUIESCE_DRAIN_GRACE_MS = 5000;
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Mirror of `engine/core/shared.js#ENGINE_DEFAULTS.shutdownTimeout` — the drain
|
|
1178
|
+
* budget a runtime uses when its config does not name one.
|
|
1179
|
+
*
|
|
1180
|
+
* Mirrored rather than imported because this script is deliberately standalone
|
|
1181
|
+
* (it must run before any Minions exists to import from). A drift test in
|
|
1182
|
+
* `test/unit/install-internal-minions.test.js` binds both this and the grace
|
|
1183
|
+
* above to their engine-side originals, so the copies cannot silently diverge.
|
|
1184
|
+
*/
|
|
1185
|
+
const ENGINE_SHUTDOWN_TIMEOUT_FALLBACK_MS = 300000;
|
|
1186
|
+
|
|
1147
1187
|
const QUIESCE_POLL_MS = 500;
|
|
1148
1188
|
const CLI_STOP_TIMEOUT_MS = 120000;
|
|
1149
1189
|
/** Budget for the read-only capability probe: `minions help` prints and exits. */
|
|
@@ -1251,7 +1291,9 @@ function probeStopCapability({ cliPath, run, env, timeoutMs = CLI_HELP_TIMEOUT_M
|
|
|
1251
1291
|
* Reports which stop contract ran and the exact argv it issued — without that,
|
|
1252
1292
|
* "the runtime did not release the database" is indistinguishable between a
|
|
1253
1293
|
* whole-stack teardown that genuinely could not finish and a legacy fallback
|
|
1254
|
-
* that only ever asked the engine to stand down.
|
|
1294
|
+
* that only ever asked the engine to stand down. It also reports the budget that
|
|
1295
|
+
* expired and where that budget came from, so "it gave up too early" is an
|
|
1296
|
+
* answerable question rather than a guess.
|
|
1255
1297
|
*/
|
|
1256
1298
|
function formatQuiescenceRefusal(quiescence = {}) {
|
|
1257
1299
|
const stopPath = quiescence.stopPath || 'none';
|
|
@@ -1262,6 +1304,9 @@ function formatQuiescenceRefusal(quiescence = {}) {
|
|
|
1262
1304
|
? ` (exit code ${quiescence.stopExitCode})`
|
|
1263
1305
|
: '';
|
|
1264
1306
|
const lines = [` Stop path: ${STOP_PATH_LABELS[stopPath] || STOP_PATH_LABELS.none}${issued}${exit}`];
|
|
1307
|
+
if (quiescence.budget) {
|
|
1308
|
+
lines.push(` Waited ${quiescence.waitedMs}ms of ${describeQuiesceBudget(quiescence.budget)}`);
|
|
1309
|
+
}
|
|
1265
1310
|
const holders = Array.isArray(quiescence.holders) ? quiescence.holders : [];
|
|
1266
1311
|
if (holders.length) {
|
|
1267
1312
|
lines.push(' Still holding the database:');
|
|
@@ -1356,6 +1401,68 @@ function assessDatabaseQuiescence({ running = [], shm = { present: false, bytes:
|
|
|
1356
1401
|
return { quiesced: true, holders: [], shmHeld: false, reason: 'no service holds engine/state.db' };
|
|
1357
1402
|
}
|
|
1358
1403
|
|
|
1404
|
+
/**
|
|
1405
|
+
* The wait budget, DERIVED from the runtime this gate is stopping.
|
|
1406
|
+
*
|
|
1407
|
+
* A fixed window cannot be right: it is the *runtime* that decides how long a
|
|
1408
|
+
* teardown legitimately takes, through `engine.shutdownTimeout` — the budget the
|
|
1409
|
+
* engine gives itself to drain pooled leases before exiting. The old fixed 60s
|
|
1410
|
+
* was shorter than the DEFAULT drain budget of five minutes, so a busy stack was
|
|
1411
|
+
* declared stuck while it was still shutting down exactly as designed, and the
|
|
1412
|
+
* migration refused a runtime that was working.
|
|
1413
|
+
*
|
|
1414
|
+
* `minions restart` already derives its teardown deadline this way; this reads
|
|
1415
|
+
* the same key, from the PINNED root, and adds the same grace — one contract,
|
|
1416
|
+
* two callers, no second number.
|
|
1417
|
+
*
|
|
1418
|
+
* Bounded on BOTH sides, because the input is operator-supplied data read from a
|
|
1419
|
+
* file this gate does not own:
|
|
1420
|
+
* - FLOOR never stricter than the fixed window it replaces
|
|
1421
|
+
* - CEILING a corrupt or hostile value cannot hang a migration indefinitely
|
|
1422
|
+
*
|
|
1423
|
+
* Never throws: a missing, unreadable, malformed, or nonsensical config is a
|
|
1424
|
+
* fallback to the engine default, not a failure. A value that is neither a
|
|
1425
|
+
* number nor a numeric string is rejected outright — `Number(true)` is `1` and
|
|
1426
|
+
* `Number([30000])` is `30000`, and silently accepting either would produce a
|
|
1427
|
+
* budget nobody configured.
|
|
1428
|
+
*
|
|
1429
|
+
* @param {object} opts
|
|
1430
|
+
* @param {string} opts.runtimeRoot the PINNED root, not the installer's own
|
|
1431
|
+
* @param {object} [opts.deps] `{ readFileSync }` injection seam
|
|
1432
|
+
* @returns {{timeoutMs:number, shutdownTimeoutMs:number, source:'config'|'default', clamped:'floor'|'ceiling'|null}}
|
|
1433
|
+
*/
|
|
1434
|
+
function resolveQuiesceTimeoutMs({ runtimeRoot, deps = {} } = {}) {
|
|
1435
|
+
const readFile = deps.readFileSync || (p => fs.readFileSync(p, 'utf8'));
|
|
1436
|
+
let shutdownTimeoutMs = ENGINE_SHUTDOWN_TIMEOUT_FALLBACK_MS;
|
|
1437
|
+
let source = 'default';
|
|
1438
|
+
try {
|
|
1439
|
+
const parsed = JSON.parse(String(readFile(path.join(runtimeRoot, 'config.json'))));
|
|
1440
|
+
const engine = parsed && typeof parsed === 'object' ? parsed.engine : null;
|
|
1441
|
+
const raw = engine && typeof engine === 'object' ? engine.shutdownTimeout : undefined;
|
|
1442
|
+
const configured = (typeof raw === 'number' || typeof raw === 'string') ? Number(raw) : NaN;
|
|
1443
|
+
if (Number.isFinite(configured) && configured > 0) {
|
|
1444
|
+
shutdownTimeoutMs = configured;
|
|
1445
|
+
source = 'config';
|
|
1446
|
+
}
|
|
1447
|
+
} catch { /* missing, unreadable, or malformed — the engine default is the safe answer */ }
|
|
1448
|
+
const derived = shutdownTimeoutMs + QUIESCE_DRAIN_GRACE_MS;
|
|
1449
|
+
const timeoutMs = Math.min(QUIESCE_TIMEOUT_CEILING_MS, Math.max(QUIESCE_TIMEOUT_FLOOR_MS, derived));
|
|
1450
|
+
let clamped = null;
|
|
1451
|
+
if (timeoutMs !== derived) clamped = timeoutMs === QUIESCE_TIMEOUT_CEILING_MS ? 'ceiling' : 'floor';
|
|
1452
|
+
return { timeoutMs, shutdownTimeoutMs, source, clamped };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** One operator-facing line for how long this gate will wait, and why. */
|
|
1456
|
+
function describeQuiesceBudget(budget = {}) {
|
|
1457
|
+
const ms = Number(budget.timeoutMs) || 0;
|
|
1458
|
+
if (budget.source === 'explicit') return `${ms}ms (caller-supplied)`;
|
|
1459
|
+
const from = budget.source === 'config'
|
|
1460
|
+
? `engine.shutdownTimeout=${budget.shutdownTimeoutMs}`
|
|
1461
|
+
: `the engine default drain of ${budget.shutdownTimeoutMs}ms`;
|
|
1462
|
+
const bound = budget.clamped ? `, clamped to the ${budget.clamped}` : '';
|
|
1463
|
+
return `${ms}ms (${from} + ${QUIESCE_DRAIN_GRACE_MS}ms grace${bound})`;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1359
1466
|
/** Block the calling thread. The installer is deliberately spawnSync-only. */
|
|
1360
1467
|
function sleepSync(ms) {
|
|
1361
1468
|
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(ms) || 0); }
|
|
@@ -1376,6 +1483,11 @@ function sleepSync(ms) {
|
|
|
1376
1483
|
* `stop`. Both paths then run the same wait loop, because the CLI's own verdict
|
|
1377
1484
|
* is never trusted — the `-shm` evidence below is.
|
|
1378
1485
|
*
|
|
1486
|
+
* The wait budget is DERIVED from the pinned runtime's own drain budget (see
|
|
1487
|
+
* `resolveQuiesceTimeoutMs`) so a stack that is legitimately slow to shut down
|
|
1488
|
+
* is waited out rather than declared stuck. An explicit `timeoutMs` is a
|
|
1489
|
+
* deliberate caller instruction and wins verbatim.
|
|
1490
|
+
*
|
|
1379
1491
|
* FAILS CLOSED: when the handles are not released inside the timeout the caller
|
|
1380
1492
|
* gets `ok: false`, the path that was taken, and the exact PIDs still holding
|
|
1381
1493
|
* the database.
|
|
@@ -1386,18 +1498,21 @@ function quiesceServices({
|
|
|
1386
1498
|
run,
|
|
1387
1499
|
env,
|
|
1388
1500
|
log = console.log,
|
|
1389
|
-
timeoutMs =
|
|
1501
|
+
timeoutMs = null,
|
|
1390
1502
|
pollMs = QUIESCE_POLL_MS,
|
|
1391
1503
|
probeTimeoutMs = CLI_HELP_TIMEOUT_MS,
|
|
1392
1504
|
deps = {},
|
|
1393
1505
|
sleep = sleepSync,
|
|
1394
1506
|
now = () => Date.now(),
|
|
1395
1507
|
} = {}) {
|
|
1508
|
+
const explicit = Number(timeoutMs);
|
|
1509
|
+
const budget = Number.isFinite(explicit) && explicit > 0
|
|
1510
|
+
? { timeoutMs: explicit, shutdownTimeoutMs: null, source: 'explicit', clamped: null }
|
|
1511
|
+
: resolveQuiesceTimeoutMs({ runtimeRoot, deps });
|
|
1396
1512
|
const probeState = () => assessDatabaseQuiescence({
|
|
1397
1513
|
running: detectRunningServices(runtimeRoot, deps),
|
|
1398
1514
|
shm: readStateShm(runtimeRoot, deps),
|
|
1399
1515
|
});
|
|
1400
|
-
const startedAt = now();
|
|
1401
1516
|
const initial = probeState();
|
|
1402
1517
|
if (initial.quiesced) {
|
|
1403
1518
|
return {
|
|
@@ -1409,10 +1524,12 @@ function quiesceServices({
|
|
|
1409
1524
|
capabilityProbe: null,
|
|
1410
1525
|
holders: [],
|
|
1411
1526
|
waitedMs: 0,
|
|
1527
|
+
budget,
|
|
1412
1528
|
reason: initial.reason,
|
|
1413
1529
|
};
|
|
1414
1530
|
}
|
|
1415
1531
|
log(` ${initial.reason}`);
|
|
1532
|
+
log(` Waiting up to ${describeQuiesceBudget(budget)}.`);
|
|
1416
1533
|
let stopped = false;
|
|
1417
1534
|
let stopPath = 'none';
|
|
1418
1535
|
let stopArgs = null;
|
|
@@ -1439,7 +1556,13 @@ function quiesceServices({
|
|
|
1439
1556
|
} else {
|
|
1440
1557
|
log(' WARNING: no installed CLI was found to run `minions stop`; waiting for the handles to be released.');
|
|
1441
1558
|
}
|
|
1442
|
-
|
|
1559
|
+
// The budget bounds THIS loop only — the capability probe and the stop
|
|
1560
|
+
// subprocess above run before it and are reported separately. Measuring the
|
|
1561
|
+
// wait from any earlier point would let a slow stop print a wait larger than
|
|
1562
|
+
// the budget that allowed it, i.e. a refusal that accuses the gate of
|
|
1563
|
+
// overrunning a deadline it actually honoured.
|
|
1564
|
+
const waitStartedAt = now();
|
|
1565
|
+
const deadline = waitStartedAt + budget.timeoutMs;
|
|
1443
1566
|
let verdict = probeState();
|
|
1444
1567
|
while (!verdict.quiesced && now() < deadline) {
|
|
1445
1568
|
sleep(pollMs);
|
|
@@ -1453,7 +1576,8 @@ function quiesceServices({
|
|
|
1453
1576
|
stopExitCode,
|
|
1454
1577
|
capabilityProbe,
|
|
1455
1578
|
holders: verdict.holders,
|
|
1456
|
-
waitedMs: now() -
|
|
1579
|
+
waitedMs: now() - waitStartedAt,
|
|
1580
|
+
budget,
|
|
1457
1581
|
reason: verdict.reason,
|
|
1458
1582
|
};
|
|
1459
1583
|
}
|
|
@@ -2960,6 +3084,12 @@ module.exports = {
|
|
|
2960
3084
|
detectRunningServices,
|
|
2961
3085
|
readStateShm,
|
|
2962
3086
|
assessDatabaseQuiescence,
|
|
3087
|
+
QUIESCE_TIMEOUT_FLOOR_MS,
|
|
3088
|
+
QUIESCE_TIMEOUT_CEILING_MS,
|
|
3089
|
+
QUIESCE_DRAIN_GRACE_MS,
|
|
3090
|
+
ENGINE_SHUTDOWN_TIMEOUT_FALLBACK_MS,
|
|
3091
|
+
resolveQuiesceTimeoutMs,
|
|
3092
|
+
describeQuiesceBudget,
|
|
2963
3093
|
WHOLE_STACK_STOP_ARGS,
|
|
2964
3094
|
LEGACY_STOP_ARGS,
|
|
2965
3095
|
CLI_CAPABILITY_PROBE_ARGS,
|
package/bin/minions.js
CHANGED
|
@@ -85,7 +85,7 @@ const {
|
|
|
85
85
|
stopStackExitCode,
|
|
86
86
|
holderIsLiveDaemon,
|
|
87
87
|
STOP_STACK_EXIT,
|
|
88
|
-
|
|
88
|
+
DRAIN_GRACE_MS: STOP_STACK_DRAIN_GRACE_MS,
|
|
89
89
|
} = require(path.join(PKG_ROOT, 'engine', 'recovery', 'stop-stack'));
|
|
90
90
|
// Dev-mode dashboard port. `--dev` mode (see argv parser below) retargets
|
|
91
91
|
// MINIONS_HOME at PKG_ROOT and routes the dashboard onto this port so a dev
|
|
@@ -681,13 +681,30 @@ function stopWholeRuntimeStack({ source, timeoutMs, waitForRelease = false }) {
|
|
|
681
681
|
return verdict;
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
-
/**
|
|
685
|
-
*
|
|
686
|
-
*
|
|
684
|
+
/** The pinned runtime's configured graceful-drain budget, falling back to the
|
|
685
|
+
* engine default when config.json is missing, unreadable, or does not name a
|
|
686
|
+
* positive one. Shared by `restart` and `stop --all` so both teardown paths
|
|
687
|
+
* hand the engine the SAME drain window its own graceful handler expects. */
|
|
688
|
+
function _resolveConfiguredShutdownTimeoutMs() {
|
|
689
|
+
try {
|
|
690
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(MINIONS_HOME, 'config.json'), 'utf8'));
|
|
691
|
+
return Number(cfg?.engine?.shutdownTimeout) || shared.ENGINE_DEFAULTS.shutdownTimeout;
|
|
692
|
+
} catch { return shared.ENGINE_DEFAULTS.shutdownTimeout; }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** `--timeout <ms>` / `--timeout=<ms>` for `minions stop --all`. With no flag it
|
|
696
|
+
* DERIVES the budget from the pinned runtime's `engine.shutdownTimeout` plus the
|
|
697
|
+
* drain grace — exactly as `minions restart` does — so a whole-stack stop is
|
|
698
|
+
* given the engine's own drain window rather than a flat default that would
|
|
699
|
+
* force-reap a slow drain mid-flight (the internal installer's quiescence gate
|
|
700
|
+
* shells `stop --all --wait` with no `--timeout` and depends on that budget
|
|
701
|
+
* reaching the teardown). Never shorter than the installer's quiescence floor:
|
|
702
|
+
* the shared module's default is that floor, and the engine drain default is
|
|
703
|
+
* larger still. */
|
|
687
704
|
function _resolveStopAllTimeoutMs(argv) {
|
|
688
705
|
const flagIdx = argv.indexOf('--timeout');
|
|
689
706
|
const inline = argv.find(a => typeof a === 'string' && a.startsWith('--timeout='));
|
|
690
|
-
if (flagIdx < 0 && !inline) return
|
|
707
|
+
if (flagIdx < 0 && !inline) return _resolveConfiguredShutdownTimeoutMs() + STOP_STACK_DRAIN_GRACE_MS;
|
|
691
708
|
const raw = flagIdx >= 0 ? argv[flagIdx + 1] : inline.slice('--timeout='.length);
|
|
692
709
|
const n = Number(raw);
|
|
693
710
|
if (!Number.isInteger(n) || n <= 0) {
|
|
@@ -1748,13 +1765,10 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1748
1765
|
const restartStartMs = Date.now();
|
|
1749
1766
|
// Give the engine's graceful handler time to drain non-reattachable pooled
|
|
1750
1767
|
// leases. Detached cold agents are not part of that drain and remain alive
|
|
1751
|
-
// for normal PID-file reattachment by the replacement engine.
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
return Number(cfg?.engine?.shutdownTimeout) || shared.ENGINE_DEFAULTS.shutdownTimeout;
|
|
1756
|
-
} catch { return shared.ENGINE_DEFAULTS.shutdownTimeout; }
|
|
1757
|
-
})();
|
|
1768
|
+
// for normal PID-file reattachment by the replacement engine. The same budget
|
|
1769
|
+
// backs `stop --all` (see _resolveStopAllTimeoutMs) so both teardown paths
|
|
1770
|
+
// honour the runtime's own drain window.
|
|
1771
|
+
const shutdownTimeout = _resolveConfiguredShutdownTimeoutMs();
|
|
1758
1772
|
// Ordered teardown — stop-intent first so the supervisor stands down and
|
|
1759
1773
|
// cannot race-respawn what we are about to stop, then supervisor, dashboard,
|
|
1760
1774
|
// graceful engine drain, verified reap, and the late-respawn sweep that
|
|
@@ -1764,7 +1778,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1764
1778
|
// stack, and the port gate below is its real readiness condition.
|
|
1765
1779
|
const teardown = stopWholeRuntimeStack({
|
|
1766
1780
|
source: 'minions restart',
|
|
1767
|
-
timeoutMs: shutdownTimeout +
|
|
1781
|
+
timeoutMs: shutdownTimeout + STOP_STACK_DRAIN_GRACE_MS,
|
|
1768
1782
|
waitForRelease: false,
|
|
1769
1783
|
});
|
|
1770
1784
|
// A holder that is provably NOT one of our daemons (a stale PID file, a
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// dashboard/js/agent-identity.js — shared agent display-identity formatter.
|
|
2
|
+
//
|
|
3
|
+
// Single client-side seam every user-facing renderer uses to render an agent's
|
|
4
|
+
// friendly display name plus, for dynamically-created temp agents, a visible
|
|
5
|
+
// "Temp" badge. The canonical agent id (`temp-<uid>`) is NEVER shown as the
|
|
6
|
+
// primary name in normal UI, but stays discoverable via title/tooltip.
|
|
7
|
+
//
|
|
8
|
+
// The server already stamps a deterministic friendly `agentName` / `name` on
|
|
9
|
+
// temp dispatch, work-item, and roster payloads (engine/core/shared.js
|
|
10
|
+
// #tempAgentCallSign). The `callSign()` fallback below mirrors that algorithm
|
|
11
|
+
// verbatim so a legacy record missing the friendly name still renders a stable
|
|
12
|
+
// call sign rather than a raw `temp-msd…` hash. Keep this vocabulary + hash in
|
|
13
|
+
// sync with engine/core/shared.js (asserted by
|
|
14
|
+
// test/unit/temp-agent-identity.test.js).
|
|
15
|
+
(function () {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// Mirror of engine/core/shared.js TEMP_AGENT_CALL_SIGN_ADJECTIVES / _NAMES.
|
|
19
|
+
var ADJECTIVES = [
|
|
20
|
+
'Cosmic', 'Starry', 'Nebular', 'Twinkly', 'Lunar', 'Solar', 'Astral', 'Galactic',
|
|
21
|
+
'Stellar', 'Orbital', 'Meteoric', 'Cloudy', 'Sunny', 'Radiant', 'Sparkly', 'Dreamy',
|
|
22
|
+
'Breezy', 'Drifting', 'Glowing', 'Floaty', 'Skybound', 'Wandering', 'Whirling', 'Shimmery',
|
|
23
|
+
];
|
|
24
|
+
var NAMES = [
|
|
25
|
+
'Comet', 'Nova', 'Luna', 'Sol', 'Astro', 'Orion', 'Nebula', 'Pluto',
|
|
26
|
+
'Halley', 'Vega', 'Rigel', 'Sirius', 'Polaris', 'Stardust', 'Cosmo', 'Nimbus',
|
|
27
|
+
'Zephyr', 'Aurora', 'Meteor', 'Galaxy', 'Twinkle', 'Cirrus', 'Cygnus', 'Juno',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function isTemp(idOrObj) {
|
|
31
|
+
var id = _idOf(idOrObj);
|
|
32
|
+
return typeof id === 'string' && /^temp-/i.test(id.trim());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// FNV-1a 32-bit — mirror of engine/core/shared.js#_fnv1a32.
|
|
36
|
+
function _fnv1a32(str) {
|
|
37
|
+
var h = 0x811c9dc5;
|
|
38
|
+
for (var i = 0; i < str.length; i++) {
|
|
39
|
+
h ^= str.charCodeAt(i);
|
|
40
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
return h >>> 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Deterministic friendly call sign from a canonical temp id, e.g. "Cosmic Comet".
|
|
46
|
+
// Returns null for non-temp / malformed ids.
|
|
47
|
+
function callSign(idOrObj) {
|
|
48
|
+
var id = _idOf(idOrObj);
|
|
49
|
+
if (!isTemp(id)) return null;
|
|
50
|
+
var key = id.trim().toLowerCase();
|
|
51
|
+
// Single hash of the canonical id, then slice independent index streams from
|
|
52
|
+
// its low and high bits (mirror of engine/core/shared.js#tempAgentCallSign).
|
|
53
|
+
// Hashing the bare id once keeps its full entropy in BOTH indices; a trailing
|
|
54
|
+
// ":adj"/":name" discriminator would let the identical suffix dominate the
|
|
55
|
+
// FNV-1a low bits and collapse the joint distribution for the long-shared-
|
|
56
|
+
// prefix ids that concurrent temp allocation produces.
|
|
57
|
+
var h = _fnv1a32(key);
|
|
58
|
+
var adj = ADJECTIVES[h % ADJECTIVES.length];
|
|
59
|
+
var name = NAMES[Math.floor(h / ADJECTIVES.length) % NAMES.length];
|
|
60
|
+
return adj + ' ' + name;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _idOf(idOrObj) {
|
|
64
|
+
if (idOrObj && typeof idOrObj === 'object') {
|
|
65
|
+
return idOrObj.id || idOrObj.agent || idOrObj.dispatched_to || '';
|
|
66
|
+
}
|
|
67
|
+
return idOrObj;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Friendly display name string (UNESCAPED — callers escape). Prefers a
|
|
71
|
+
// server-supplied friendly name, then the deterministic call sign, then the
|
|
72
|
+
// canonical id. Never leads with `temp-…` when a call sign is derivable.
|
|
73
|
+
function displayName(idOrObj) {
|
|
74
|
+
var provided = null;
|
|
75
|
+
var id = _idOf(idOrObj);
|
|
76
|
+
if (idOrObj && typeof idOrObj === 'object') {
|
|
77
|
+
provided = idOrObj.name || idOrObj.agentName || idOrObj.displayName || null;
|
|
78
|
+
}
|
|
79
|
+
if (isTemp(id)) {
|
|
80
|
+
// A stored friendly name wins (stable across the record's life); otherwise
|
|
81
|
+
// derive deterministically. Guard against a legacy raw `temp-…` slipping
|
|
82
|
+
// through as `provided`.
|
|
83
|
+
if (provided && !/^temp-/i.test(String(provided).trim())) return provided;
|
|
84
|
+
return callSign(id) || id || '';
|
|
85
|
+
}
|
|
86
|
+
return provided || id || '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var _esc = (typeof escapeHtml === 'function')
|
|
90
|
+
? escapeHtml
|
|
91
|
+
: function (s) {
|
|
92
|
+
return String(s == null ? '' : s)
|
|
93
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
94
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Static, XSS-safe "Temp" badge markup. `title` carries the canonical id for
|
|
98
|
+
// debugging; the visible + aria-labelled "Temp" text keeps the temporary
|
|
99
|
+
// status unmistakable to sighted and assistive-tech users alike.
|
|
100
|
+
function badgeHtml(idOrObj) {
|
|
101
|
+
var id = _idOf(idOrObj);
|
|
102
|
+
var title = id ? ' title="Temporary agent — ' + _esc(id) + '"' : ' title="Temporary agent"';
|
|
103
|
+
return '<span class="agent-temp-badge" role="img" aria-label="Temporary agent"' + title + '>Temp</span>';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Escaped friendly name + trailing Temp badge (for temp agents). Safe to
|
|
107
|
+
// inject as innerHTML. This is the single formatter renderers should use.
|
|
108
|
+
function nameWithBadgeHtml(idOrObj) {
|
|
109
|
+
var name = _esc(displayName(idOrObj));
|
|
110
|
+
if (isTemp(idOrObj)) return name + ' ' + badgeHtml(idOrObj);
|
|
111
|
+
return name;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
window.MinionsAgentIdentity = {
|
|
115
|
+
isTemp: isTemp,
|
|
116
|
+
callSign: callSign,
|
|
117
|
+
displayName: displayName,
|
|
118
|
+
badgeHtml: badgeHtml,
|
|
119
|
+
nameWithBadgeHtml: nameWithBadgeHtml,
|
|
120
|
+
};
|
|
121
|
+
})();
|
package/dashboard/js/modal.js
CHANGED
|
@@ -14,6 +14,10 @@ function closeModal() {
|
|
|
14
14
|
}
|
|
15
15
|
const modalEl = document.querySelector('#modal .modal');
|
|
16
16
|
if (modalEl) modalEl.classList.remove('modal-wide');
|
|
17
|
+
// The modal subtree is about to be hidden — dismiss any transient popup
|
|
18
|
+
// anchored inside it while contains() can still identify its anchor. Scoped,
|
|
19
|
+
// so a popover anchored elsewhere on the page is untouched.
|
|
20
|
+
if (typeof _dismissModalTransientPopups === 'function') _dismissModalTransientPopups();
|
|
17
21
|
document.getElementById('modal').classList.remove('open');
|
|
18
22
|
if (typeof popModalFrame === 'function') popModalFrame();
|
|
19
23
|
else if (typeof clearModalBackStack === 'function') clearModalBackStack();
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -202,7 +202,11 @@ const RENDER_VERSIONS = {
|
|
|
202
202
|
// Bumped 22→23: 'paused' joins the lifecycle order between failed and the
|
|
203
203
|
// terminal tail (it was unranked, so it sorted behind every done row in both
|
|
204
204
|
// directions) — row ORDER changes again for byte-identical input.
|
|
205
|
-
|
|
205
|
+
// Bumped 23→24 (W-mscjvm33007r79ef): per-item actions (except Delete) moved
|
|
206
|
+
// into an overflow (⋯) menu and an opt-in bulk-selection mode adds an optional
|
|
207
|
+
// leading checkbox column + row-selected styling — row MARKUP changes for
|
|
208
|
+
// byte-identical input.
|
|
209
|
+
workItems: 24,
|
|
206
210
|
skills: 1,
|
|
207
211
|
commands: 1,
|
|
208
212
|
mcpServers: 1,
|
|
@@ -519,6 +523,21 @@ function _refreshSidebarCounterSummaries() {
|
|
|
519
523
|
// entry read it — drop the dead endpoint + fetch. (Review finding #9.)
|
|
520
524
|
}
|
|
521
525
|
|
|
526
|
+
function _setToolsRefreshStatus(state) {
|
|
527
|
+
var el = document.getElementById('tools-refresh-status');
|
|
528
|
+
if (!el) return;
|
|
529
|
+
var text = '';
|
|
530
|
+
if (state === 'loading') {
|
|
531
|
+
text = 'Discovering skills, commands & MCP servers…';
|
|
532
|
+
} else if (state === 'error-stale') {
|
|
533
|
+
text = 'Showing last known inventory — refresh failed. Retrying…';
|
|
534
|
+
} else if (state === 'error-cold') {
|
|
535
|
+
text = 'Could not load skills inventory. Retrying…';
|
|
536
|
+
}
|
|
537
|
+
el.textContent = text;
|
|
538
|
+
el.style.display = text ? 'block' : 'none';
|
|
539
|
+
}
|
|
540
|
+
|
|
522
541
|
function _renderToolsInventory(tools) {
|
|
523
542
|
if (!tools || !Array.isArray(tools.skills)
|
|
524
543
|
|| !Array.isArray(tools.commands)
|
|
@@ -988,20 +1007,36 @@ function _processStatusUpdate(data, opts) {
|
|
|
988
1007
|
// Skills/commands/MCP discovery runs in a child process behind /api/tools.
|
|
989
1008
|
// It is only consumed by the Tools page, so every other page skips the
|
|
990
1009
|
// request and /api/status stays a small, non-blocking health envelope.
|
|
1010
|
+
//
|
|
1011
|
+
// Perceived latency: /api/tools serves a cached inventory immediately and
|
|
1012
|
+
// refreshes in the background (stale-while-revalidate on the server), so the
|
|
1013
|
+
// page renders from last-known data instead of blocking on the multi-second
|
|
1014
|
+
// child scan. The status line below represents the only user-visible states:
|
|
1015
|
+
// the true cold-start wait (no data yet) and a refresh failure while stale
|
|
1016
|
+
// data is shown.
|
|
991
1017
|
_safeRender('toolsInventory', function() {
|
|
992
1018
|
if (typeof currentPage !== 'undefined' && currentPage !== 'tools') return;
|
|
993
1019
|
const seq = (window._refreshSeq = (window._refreshSeq || 0) + 1);
|
|
994
1020
|
window._lastRequestedSeq = window._lastRequestedSeq || {};
|
|
995
1021
|
window._lastRequestedSeq.toolsInventory = seq;
|
|
1022
|
+
if (!window._lastToolsInventory) _setToolsRefreshStatus('loading');
|
|
996
1023
|
_condFetchJson('/api/tools')
|
|
997
1024
|
.then(function(resp) {
|
|
998
|
-
if (seq < (window._lastRequestedSeq.toolsInventory || 0)
|
|
1025
|
+
if (seq < (window._lastRequestedSeq.toolsInventory || 0)) return;
|
|
1026
|
+
if (resp.notModified) { _setToolsRefreshStatus('idle'); return; }
|
|
999
1027
|
const tools = resp.data;
|
|
1000
1028
|
if (_renderToolsInventory(tools)) window._lastToolsInventory = tools;
|
|
1029
|
+
_setToolsRefreshStatus('idle');
|
|
1001
1030
|
})
|
|
1002
1031
|
.catch(function() {
|
|
1032
|
+
if (seq < (window._lastRequestedSeq.toolsInventory || 0)) return;
|
|
1003
1033
|
const tools = window._lastToolsInventory;
|
|
1004
|
-
if (tools)
|
|
1034
|
+
if (tools) {
|
|
1035
|
+
_renderToolsInventory(tools);
|
|
1036
|
+
_setToolsRefreshStatus('error-stale');
|
|
1037
|
+
} else {
|
|
1038
|
+
_setToolsRefreshStatus('error-cold');
|
|
1039
|
+
}
|
|
1005
1040
|
});
|
|
1006
1041
|
});
|
|
1007
1042
|
// Harness propagation diagnostic comes from /api/harness/diagnostics.
|
|
@@ -80,7 +80,7 @@ function renderAgents(agents) {
|
|
|
80
80
|
grid.innerHTML = agents.map(a => `
|
|
81
81
|
<div class="agent-card ${statusColor(a.status)}" data-agent-id="${escapeHtml(a.id)}" onclick="if(shouldIgnoreSelectionClick(event))return;openAgentDetail(this.dataset.agentId)">
|
|
82
82
|
<div class="agent-card-header">
|
|
83
|
-
<span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${
|
|
83
|
+
<span class="agent-name"><span class="agent-emoji">${escapeHtml(a.emoji)}</span>${MinionsAgentIdentity.nameWithBadgeHtml(a)}${_runtimeTagHtml(a.runtime)}${_modelChipHtml(a.displayModel || a.model)}</span>
|
|
84
84
|
<span class="status-badge ${escapeHtml(a.status)}">${escapeHtml(a.status)}</span>
|
|
85
85
|
</div>
|
|
86
86
|
<div class="agent-role">${escapeHtml(a.role)}</div>
|
|
@@ -158,9 +158,21 @@ async function openAgentDetail(id) {
|
|
|
158
158
|
modelSpan.textContent = modelLabel;
|
|
159
159
|
const children = [
|
|
160
160
|
emojiSpan,
|
|
161
|
-
document.createTextNode(' ' + (agent.name || '') + ' \u2014 ' + (agent.role || '')),
|
|
161
|
+
document.createTextNode(' ' + (MinionsAgentIdentity.displayName(agent) || agent.name || '') + ' \u2014 ' + (agent.role || '')),
|
|
162
162
|
runtimeSpan,
|
|
163
163
|
];
|
|
164
|
+
// Temp agents get a visible + aria-labelled "Temp" badge; the canonical id
|
|
165
|
+
// stays in the title for debugging. Built via DOM so no user data is ever
|
|
166
|
+
// interpreted as HTML (SEC-03 detail-header contract).
|
|
167
|
+
if (MinionsAgentIdentity.isTemp(agent)) {
|
|
168
|
+
const tempBadge = document.createElement('span');
|
|
169
|
+
tempBadge.className = 'agent-temp-badge';
|
|
170
|
+
tempBadge.setAttribute('role', 'img');
|
|
171
|
+
tempBadge.setAttribute('aria-label', 'Temporary agent');
|
|
172
|
+
tempBadge.title = 'Temporary agent — ' + (agent.id || '');
|
|
173
|
+
tempBadge.textContent = 'Temp';
|
|
174
|
+
children.push(tempBadge);
|
|
175
|
+
}
|
|
164
176
|
children.push(modelSpan);
|
|
165
177
|
nameEl.replaceChildren(...children);
|
|
166
178
|
|
|
@@ -303,7 +303,7 @@ function renderDispatch(dispatch, opts) {
|
|
|
303
303
|
const dispatchItemHtml = (d, trailing) =>
|
|
304
304
|
'<div class="dispatch-item">' +
|
|
305
305
|
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
306
|
-
'<span class="dispatch-agent">' +
|
|
306
|
+
'<span class="dispatch-agent">' + MinionsAgentIdentity.nameWithBadgeHtml({ id: d.agent, name: d.agentName }) + '</span>' +
|
|
307
307
|
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
308
308
|
renderStuckChip(d) +
|
|
309
309
|
trailing +
|
|
@@ -359,7 +359,7 @@ function renderDispatch(dispatch, opts) {
|
|
|
359
359
|
: '';
|
|
360
360
|
return '<tr>' +
|
|
361
361
|
'<td><span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span></td>' +
|
|
362
|
-
'<td>' +
|
|
362
|
+
'<td>' + MinionsAgentIdentity.nameWithBadgeHtml({ id: d.agent, name: d.agentName }) + '</td>' +
|
|
363
363
|
'<td style="width:100%;max-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml((d.task || '').slice(0, 60)) + '</td>' +
|
|
364
364
|
'<td style="color:' + (d.result === 'success' ? 'var(--green)' : 'var(--red)') + '">' + escHtml(d.result || '') + errorBtn + '</td>' +
|
|
365
365
|
'<td class="pr-date">' + shortTime(d.completed_at) + '</td>' +
|