diff --git a/Plugin/unraid/Tools/ai_chat_worker.php b/Plugin/unraid/Tools/ai_chat_worker.php index da713d7..8f53b9a 100644 --- a/Plugin/unraid/Tools/ai_chat_worker.php +++ b/Plugin/unraid/Tools/ai_chat_worker.php @@ -403,6 +403,16 @@ if ($diagnostic) { } } +// The machine itself, separate from the AI subsystem's own health above. Read from the cache the +// WebGUI already writes, so it costs a file read rather than a docker ps and a disk sweep. +if ($can('system_state')) { + $state = vv_ai_system_state(); + if ($state !== '') { + $attached['system'] = 'machine state'; + $diagBlock .= $state; + } +} + // The troubleshooting profile gets the actual tail of the one log the operator is looking at, // warnings and ordinary lines alike. The fleet-wide WARN/ERROR sweep above cannot answer "why // did this one stop" — the last line a script printed before dying is usually not labelled. @@ -628,6 +638,18 @@ if ($profile === 'chat') { . "which line shows it. 'This looks like a Varaverk problem' with nothing behind it " . "sends someone hunting through code they did not write and cannot fix, which is " . "the least useful place you can send them.\n\n" + . "TELL THEM WHERE TO CLICK, NOT WHICH FILE TO EDIT\n" + . "You cannot change any setting yourself in this mode, and that is the point: the " + . "fix is theirs to make. Nearly every setting has a control in the web UI, and the " + . "passages include a map of where each one lives — use it. Give the route the way " + . "you would say it out loud: which tab, which card, which control.\n\n" + . " Daily container updates are off now. Scheduler tab → Daily Sync Maintenance → " + . "Steps → toggle Docker Update off.\n\n" + . "Name the conf key as well, so they can confirm they changed the right thing — but " + . "the route comes first and the file is the footnote. Telling someone to edit " + . "master.conf over SSH when a switch exists is worse advice: it is slower, it skips " + . "the backup and the syntax check that the UI write does for them, and it is how a " + . "typo takes the array down. Say a setting has no control only when the map says so.\n\n" . "Do not suggest editing conf files by hand. Settings on this page have controls, " . "and the operator is reading this inside the WebGUI. Name the control.\n\n" . "IF IT REALLY IS A DEFECT, FILE IT\n" diff --git a/Plugin/unraid/include/ai.php b/Plugin/unraid/include/ai.php index 5f24aa2..96b8a74 100644 --- a/Plugin/unraid/include/ai.php +++ b/Plugin/unraid/include/ai.php @@ -1372,6 +1372,87 @@ function vv_ai_bug_report(array $b): string { return $out; } +// The machine as it is right now — hardware, containers, storage — summarised small enough to +// sit in a prompt. +// +// Read from the cache the WebGUI already writes every minute rather than measured here. A +// question must not cost a docker ps across fifty containers plus a disk sweep; the numbers move +// on their own schedule and one minute stale is the same answer. The age is stated so the model +// can say "a minute ago" rather than implying it looked just now. +// +// Strictly read-only, and there is no counterpart that changes any of it. Knowing a container is +// down is what lets an explanation be about this machine instead of about Unraid in general; +// restarting it is a decision that belongs to a person looking at the screen. +function vv_ai_system_state(): string { + $p = '/tmp/varaverk/api/monitor.json'; + if (!is_readable($p)) return ''; + $d = json_decode((string) @file_get_contents($p), true); + if (!is_array($d)) return ''; + + $age = time() - (int) @filemtime($p); + $s = "LIVE MACHINE STATE (read-only, measured " . ($age < 90 ? 'under a minute' : round($age / 60) . ' minutes') + . " ago — you cannot change any of it)\n"; + + $sys = $d['system'] ?? []; + if ($sys) { + $s .= '- host: ' . ($sys['cpu_model'] ?? '?') . ', ' . ($sys['cpu_threads'] ?? '?') . " threads\n"; + } + if (!empty($d['cpu']['overall'])) $s .= '- cpu: ' . $d['cpu']['overall'] . "% overall\n"; + + $r = $d['resources'] ?? []; + if (!empty($r['ram_total_mb'])) { + $used = (int) $r['ram_total_mb'] - (int) ($r['ram_free_mb'] ?? 0); + $s .= sprintf("- memory: %d of %d MB used\n", $used, (int) $r['ram_total_mb']); + } + + foreach ((array) ($d['gpus'] ?? []) as $g) { + $s .= sprintf("- gpu %d: %s, %d/%d MB, %d%% util, %d°C\n", $g['index'] ?? 0, $g['name'] ?? '?', + $g['memory_used'] ?? 0, $g['memory_total'] ?? 0, $g['utilization'] ?? 0, + $g['temperature'] ?? 0); + } + + // Named individually only when something is wrong with them. Fifty healthy container names is + // the bulk of this block and none of it is ever the answer; the handful that are not are. + // + // Taken from the payload's own `stopped` list rather than derived. There is no state field on + // a container row — only `status`, which reads "Up 15 hours (healthy)" — so an earlier version + // of this looked for "run", found it nowhere, and reported all fifty as down. A block that + // confidently lists healthy containers as stopped is worse than no block: it is exactly the + // sort of thing the model would build a whole wrong explanation on. + $ctrs = (array) ($d['containers'] ?? []); + $stopped = (array) ($d['stopped'] ?? []); + $down = array_values(array_filter(array_map( + fn($c) => trim((string) (is_array($c) ? ($c['name'] ?? '') : $c)), $stopped))); + + // Up, but failing its own healthcheck. Running is not the same as working, and this is the + // state that produces "it is started, so why is nothing happening". + $sick = []; + foreach ($ctrs as $c) { + if (stripos((string) ($c['status'] ?? ''), 'unhealthy') !== false) $sick[] = (string) ($c['name'] ?? '?'); + } + + $s .= '- containers: ' . count($ctrs) . ' running'; + $s .= $down ? ', STOPPED: ' . implode(', ', array_slice($down, 0, 12)) : ', none stopped'; + $s .= $sick ? '; UNHEALTHY: ' . implode(', ', array_slice($sick, 0, 8)) : ''; + $s .= "\n"; + + foreach ((array) ($d['storage'] ?? []) as $pool) { + if (empty($pool['name'])) continue; + $s .= sprintf("- pool %s: %s%% used\n", $pool['name'], $pool['used_pct'] ?? $pool['pct'] ?? '?'); + } + + $par = $d['parity'] ?? []; + if ($par) { + $s .= '- array: ' . (($par['num_missing'] ?? 0) ? $par['num_missing'] . ' disks MISSING' : 'no missing disks') + . (($par['num_disabled'] ?? 0) ? ', ' . $par['num_disabled'] . ' disabled' : '') + . (!empty($par['in_progress']) ? ', parity operation in progress' : '') . "\n"; + } + if (!empty($d['ups']['available'])) { + $s .= '- ups: ' . ($d['ups']['status'] ?? '?') . "\n"; + } + return $s . "\n"; +} + // Where a report can go from this install, resolved once so the page and the sender agree. // // Local is opt-in and off by default, because a default that files into the operator's own diff --git a/Plugin/unraid/include/ai_profiles.php b/Plugin/unraid/include/ai_profiles.php index b92dae5..155107e 100644 --- a/Plugin/unraid/include/ai_profiles.php +++ b/Plugin/unraid/include/ai_profiles.php @@ -113,7 +113,11 @@ const VV_AI_PROFILES_DEF = [ 'hint' => 'Reasons from evidence — the log you have open, or one you name. Says so when there is none.', 'turns' => 2, 'ui' => true, - 'caps' => ['retrieve', 'health', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', 'file_bugs'], + // system_state is read-only and shared with repair. Both need to know a container is down + // or a pool is full to explain anything about this machine rather than about Unraid in + // general; neither gets a way to act on it, and only repair can change a setting. + 'caps' => ['retrieve', 'health', 'system_state', 'run_evidence', 'scoped_log', + 'incidents', 'conf_lookup', 'file_bugs'], ], // The only profile that may change a setting, and the only one not offered as a button. // @@ -132,8 +136,8 @@ const VV_AI_PROFILES_DEF = [ 'hint' => 'Works through a finding with you, and can apply a fix you approve.', 'turns' => 3, 'ui' => false, - 'caps' => ['retrieve', 'health', 'run_evidence', 'scoped_log', 'incidents', 'conf_lookup', - 'conf_write', 'probe', 'file_findings', 'phrasebook', 'past_fixes'], + 'caps' => ['retrieve', 'health', 'system_state', 'run_evidence', 'scoped_log', 'incidents', + 'conf_lookup', 'conf_write', 'probe', 'file_findings', 'phrasebook', 'past_fixes'], ], ]; @@ -142,7 +146,8 @@ const VV_AI_PROFILES_DEF = [ const VV_AI_CAP_MEANING = [ 'retrieve' => 'retrieval passages from the documentation index', 'kind_filter' => 'the retrieval kind filter the page exposes', - 'health' => 'live health sweep measured at question time', + 'health' => 'live health sweep measured at question time — the AI subsystem only', + 'system_state' => 'read-only view of the machine: hardware, containers, pools, array, UPS', 'run_evidence' => 'run record and log tail for a script named in the question', 'scoped_log' => 'log tail for whatever the operator currently has open', 'incidents' => 'operator-written history of what previously went wrong with this thing',