diff --git a/Plugin/unraid/Tools/ai_chat_worker.php b/Plugin/unraid/Tools/ai_chat_worker.php index a153e56..cf2ec3f 100644 --- a/Plugin/unraid/Tools/ai_chat_worker.php +++ b/Plugin/unraid/Tools/ai_chat_worker.php @@ -265,14 +265,40 @@ $runOutcome = $namedTarget !== '' . 'last night|duration|how long|rundown|summar\w*|recap|status|results?|outcome)\b/i', $question); -// Permission first, need second: the capability decides whether live state may be attached at -// all, and only then does the phrasing decide whether this particular question warrants it. -// troubleshoot needs no phrasing test — the operator opened a log to get there. -$diagnostic = $can('health') && ($profile === 'troubleshoot' || $runOutcome || (bool)preg_match( +// The target and its run record are resolved before the diagnostic decision rather than after, +// because whether the run failed is the single most useful input to that decision and it costs +// one small JSON read to know it. +$runTarget = ($can('scoped_log') && $scope !== '') ? $scope : ''; +if ($runTarget === '' && $runOutcome) $runTarget = $namedTarget; + +$rec = ($runTarget !== '' && vv_ai_scope_ok($runTarget)) + ? vv_ai_run_record($runTarget) + : ['ok' => false]; + +// A run that ended, reported ok and exited zero. Anything else — a bad exit, a missing end time, +// no record at all — is not clean and is treated as worth investigating. +$runClean = $rec['ok'] && $rec['status'] === 'ok' && $rec['exit'] === 0 && $rec['end'] !== null; + +$kwDiagnostic = (bool)preg_match( '/\b(why|fail(ed|ing|ure)?|error|broken?|not work|isn.t work|wrong|stuck|hang|' . 'never runs?|didn.t|won.t|debug|troubleshoot|diagnos)/i', $question -)); +); + +// Permission first, need second: the capability decides whether live state may be attached at +// all, and only then does the phrasing decide whether this particular question warrants it. +// troubleshoot needs no phrasing test — the operator opened a log to get there. +// +// A run-outcome question earns live state only when the run was not clean. "How did the daily +// orch go" about a run that exited 0 was pulling in a fleet-wide health sweep and forty recent +// warning lines to answer a question the run record and the log tail already answer completely — +// roughly two thousand tokens taken from the passages to say nothing. When the run did fail, all +// of that is exactly what explains why, so it stays. +$diagnostic = $can('health') && ( + $profile === 'troubleshoot' + || $kwDiagnostic + || ($runOutcome && !$runClean) +); $diagBlock = ''; if ($diagnostic) { @@ -301,19 +327,12 @@ if ($diagnostic) { // 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. -// The target is whatever the operator has open, and failing that whatever they named in the -// question. The second half is what makes a run-outcome question work from any view: asking how -// the daily orchestrator went while looking at the suggestions list is the ordinary case, not an -// edge one, and requiring them to open the log first is asking them to do the lookup themselves. -$runTarget = ($can('scoped_log') && $scope !== '') ? $scope : ''; -if ($runTarget === '' && $runOutcome) $runTarget = $namedTarget; - +// $runTarget and $rec were resolved above, where the diagnostic decision needed them. The record +// is emitted before the tail because it states the outcome where the tail only implies it: a log +// ending on a tidy summary block looks identical whether the script exited 0 or was killed on the +// next line, and the difference is the whole answer. $scopedLog = null; if ($runTarget !== '' && vv_ai_scope_ok($runTarget)) { - // The record first: it states the outcome, where the tail only implies it. A log that ends - // on a tidy summary block looks identical whether the script exited 0 or was killed on the - // next line, and the difference is the whole answer. - $rec = vv_ai_run_record($runTarget); if ($rec['ok']) { $attached['run_record'] = $rec['status'] . ', exit ' . var_export($rec['exit'], true); $diagBlock .= 'RUN RECORD for ' . $runTarget . " (authoritative — how the last run ended)\n" diff --git a/Plugin/unraid/Tools/ai_explain_fixtures.txt b/Plugin/unraid/Tools/ai_explain_fixtures.txt index 706e823..40af487 100644 --- a/Plugin/unraid/Tools/ai_explain_fixtures.txt +++ b/Plugin/unraid/Tools/ai_explain_fixtures.txt @@ -41,6 +41,14 @@ how long did the daily orch take | varaverk | | | run=yes has=run_record # Works from the AI tab, which sends no scope at all — resolution is from the question, not the page. how did the daily orch go last run | varaverk | | | target=Orchestrators/daily_sync_maintenance has=run_record +# ── A clean run earns the record and the log, and nothing else ───────────────────────────────── +# The health sweep and the recent-warning lines are ~660 tokens that answer nothing when the run +# exited 0, taken straight out of the passages' budget. They come back the moment it did not. +lets check the daily orch logs and see how it went | varaverk | Scheduler | | diag=no hasnt=health,warnings has=run_record,log_tail +how did the daily orch go | varaverk | | | diag=no hasnt=health,warnings +why did the daily orch fail | varaverk | | | diag=yes has=health,warnings,log_tail +why did the weekly orch fail | varaverk | | | diag=yes has=health,warnings + # ── Definitional questions must NOT be answered with last night's log ────────────────────────── what does the daily orchestrator do | varaverk | | | target=Orchestrators/daily_sync_maintenance run=no hasnt=run_record,log_tail how does the daily orch work | varaverk | | | run=no hasnt=run_record,log_tail @@ -51,6 +59,15 @@ what is the daily orchestrator | varaverk | | | run=no hasnt=run_record # about, which is indistinguishable from a right answer unless you already knew. how did sync go | varaverk | | | target=none run=no what does RSYNC_ENABLED do | varaverk | | | target=none run=no has=conf_keys +# Whole words only. "fail" contains "ai", and ai.log is a real log id — a substring match here +# attached the AI's own log to questions about everything else. +why did this run fail | varaverk | | | target=none +is the mail relay working again | varaverk | | | target=none + +# ── The Recent Activity buttons: one click from a row to an answer about that run ────────────── +# Both open the log first, so the dock is scoped to troubleshoot against that script's log id. +Why did this run fail? | troubleshoot | Orchestrators/daily_sync_maintenance | | diag=yes has=log_tail,run_record +How did this run go? | troubleshoot | Orchestrators/weekly_sync_maintenance | | has=log_tail,run_record # ── General Chat holds nothing, and hands Varaverk questions up rather than deferring ────────── how was your day | chat | | | profile=chat caps=none hasnt=health,log_tail,incidents,conf_keys diff --git a/Plugin/unraid/css/varaverk.css b/Plugin/unraid/css/varaverk.css index ade2e32..7c2f0f0 100644 --- a/Plugin/unraid/css/varaverk.css +++ b/Plugin/unraid/css/varaverk.css @@ -635,6 +635,15 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r .vv-activity-ago { color: #555; font-size: 10px; white-space: nowrap; flex-shrink: 0; } .vv-activity-dur { color: #444; font-size: 10px; white-space: nowrap; flex-shrink: 0; min-width: 32px; text-align: right; } +/* Hands a run straight to the assistant. Hidden until the row is hovered — every row carries one + and shown always they read as the point of the list, which is the run history, not the AI. */ +.vv-activity-ask { flex-shrink: 0; visibility: hidden; font: inherit; font-size: 10px; + color: #5a7a8a; background: #12191d; border: 1px solid #24343d; + border-radius: 3px; padding: 1px 6px; cursor: pointer; } +.vv-activity-row:hover .vv-activity-ask { visibility: visible; } +.vv-activity-ask:hover { color: #8fb0c4; border-color: #2d4a6a; } +.vv-activity-ask.vv-ask-bad { color: #c88; border-color: #4a2a2a; background: #1d1212; } +.vv-activity-row:focus-within .vv-activity-ask { visibility: visible; } /* Log search */ #vv-log-search { width: 120px; font-size: 11px; padding: 2px 6px; diff --git a/Plugin/unraid/include/ai.php b/Plugin/unraid/include/ai.php index db045fa..b7965db 100644 --- a/Plugin/unraid/include/ai.php +++ b/Plugin/unraid/include/ai.php @@ -855,10 +855,19 @@ function vv_ai_resolve_run_target(string $question): string { } // Anything with a log but no alias — named outright, either as an id or a bare script name. + // + // Whole words only. A substring test matched the `ai` log inside the word "f-ai-l", so + // "why did this run fail" resolved to ai.log and would have attached it to a question about + // something else entirely. Short ids make that failure common rather than exotic: mail, again, + // available, maintenance all contain it. $hits = []; foreach (vv_ai_log_ids() as $id) { - $bare = basename($id); - if (str_contains($q, strtolower($id)) || str_contains($q, strtolower($bare))) $hits[] = $id; + foreach ([strtolower($id), strtolower(basename($id))] as $needle) { + if ($needle !== '' && preg_match('/\b' . preg_quote($needle, '/') . '\b/', $q)) { + $hits[] = $id; + break; + } + } } $hits = array_unique($hits); if (count($hits) === 1) return reset($hits); diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index acd0daf..814e82d 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -2437,6 +2437,25 @@ function vvAiDockSend() { }).catch(e => vvAiDockDone('Request failed: ' + e, true)); } +// Asks about one run from the Recent Activity list, in a single click. +// +// It opens the log first rather than asking from wherever the operator happens to be standing. +// That is not decoration: vvOpenRight() runs vvShowLogMode(), which scopes the dock to +// troubleshoot against this script's log id — the profile that gets the log tail and may file a +// bug. Asking without it would send the question up under the assistant's contract with no log +// attached, which is the failure this whole day was spent removing. +// +// The question is posted as text the operator can see in the transcript, not hidden in the +// request, so what was asked on their behalf is never a mystery. +function vvAiAskRun(id, failed, ev) { + if (ev) ev.stopPropagation(); + if (!vvAiDockOn() || vvAiBusy) return; + vvOpenRight(id); + const input = document.getElementById('vv-ai-dock-input'); + input.value = failed ? 'Why did this run fail?' : 'How did this run go?'; + vvAiDockSend(); +} + function vvAiDockPoll(token, started) { started = started || Date.now(); clearTimeout(vvAiPoll); @@ -3034,16 +3053,29 @@ function vvLoadRecentActivity() { return; } const now = Math.floor(Date.now() / 1000); + const withAi = vvAiDockOn(); let errors = 0; let html = '