Add an incident journal the troubleshooter reads back, and thinking for diagnosis

This commit is contained in:
Gmer4Lfe
2026-08-05 20:26:56 -04:00
parent 5a813eb4c8
commit 714d97dd49
4 changed files with 159 additions and 2 deletions
+14
View File
@@ -189,6 +189,20 @@ if ($profile === 'troubleshoot' && $scope !== '') {
}
}
// What has gone wrong with this same thing before, and what actually fixed it. Operator-written,
// so it outranks anything the model would infer from the log — it is the only input here that
// records a confirmed outcome rather than a reading of evidence.
if ($scope !== '') {
$past = vv_ai_incidents_for($scope, 4);
if ($past) {
$diagBlock .= "PREVIOUSLY ON THIS, WRITTEN BY THE OPERATOR AFTER IT WAS RESOLVED\n"
. "Confirmed outcomes, not guesses. If the current symptom matches one of "
. "these, say so and lead with it. If it clearly does not, ignore them "
. "rather than forcing a fit.\n\n"
. implode("\n", $past) . "\n\n";
}
}
// Where a named conf key really lives, resolved before the model sees the question. Deterministic
// so the answer cannot be a guess: the operator may be certain a setting is in master.conf when
// it is in the host conf, and the useful reply names the file and line rather than not finding it.
+12 -1
View File
@@ -187,6 +187,17 @@ if ($action === 'clear') {
exit;
}
// ── incident_add ──────────────────────────────────────────────────────────────
// Appends one operator-written "this was the fix" note against a scope. POST only, and the
// scope is whitelisted the same way ask's is — it is written to a file that later rides in a
// prompt, so it gets the same treatment as anything else that reaches the model.
if ($action === 'incident_add') {
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
echo json_encode(vv_ai_incident_add(
trim($_POST['scope'] ?? ''), trim($_POST['symptom'] ?? ''), trim($_POST['fix'] ?? '')));
exit;
}
// ── ask ───────────────────────────────────────────────────────────────────────
if ($action === 'ask') {
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
@@ -220,7 +231,7 @@ if ($action === 'ask') {
// richer than a file name is an instruction-injection surface for no benefit — a scope is
// only ever a name from this page's own view state.
$scope = trim($_POST['scope'] ?? '');
if ($scope !== '' && !preg_match('#^[A-Za-z0-9 ._/-]{1,80}$#', $scope)) $scope = '';
if ($scope !== '' && !vv_ai_scope_ok($scope)) $scope = '';
// The retrieval filter only means anything to the profile that retrieves.
$kind = $profile === 'varaverk' ? trim($_POST['kind'] ?? '') : '';
+79
View File
@@ -781,6 +781,85 @@ function vv_ai_find_conf_key(string $key): array {
return ['ok' => false];
}
// One definition of a valid scope, used by every caller. A scope names something in this page's
// own view state — a conf file, a script, a log. It reaches the model as text and, for the
// troubleshooting profile, composes a path under LOG_DIR, so `..` is refused outright rather
// than left for the containment check downstream to catch. That check stays: this is the first
// gate, not the only one.
function vv_ai_scope_ok(string $scope): bool {
if (!preg_match('#^[A-Za-z0-9 ._/-]{1,80}$#', $scope)) return false;
if (str_contains($scope, '..')) return false;
return true;
}
// ── Incident journal ─────────────────────────────────────────────────────────────────────────
// "We have seen this before, and here is what it was." Appended as you work through logs, and
// fed back the next time the same script is being diagnosed.
//
// Deliberately NOT part of the AI index. The index reads git-tracked files only, and this lives
// under data/ which is gitignored — which is correct twice over: incident notes are about this
// installation and should not be pushed, and retrieval by similarity is the wrong lookup here.
// The right question is "what has gone wrong with THIS script before", which is an exact match
// on the scope, not a vector search. Same reasoning as the standing memory file.
//
// Markdown rather than a delimited .db because the useful part is prose — a fix is a sentence,
// not a field — and it stays hand-editable when a note turns out to be wrong.
function vv_ai_incidents_path(): string {
return DATA_DIR . '/ai_incidents.md';
}
// One entry, appended. The symptom is captured from what was being asked; the fix is written by
// the operator. That split matters: the model's diagnosis is a hypothesis, and writing a
// hypothesis into institutional memory as fact is how a wrong answer outlives the incident.
function vv_ai_incident_add(string $scope, string $symptom, string $fix): array {
$scope = trim($scope);
$symptom = trim($symptom);
$fix = trim($fix);
if ($scope === '' || $fix === '') return ['ok' => false, 'error' => 'scope and fix are required'];
if (!vv_ai_scope_ok($scope)) return ['ok' => false, 'error' => 'bad scope'];
$entry = "\n## " . date('Y-m-d') . ' · ' . $scope . "\n"
. ($symptom !== '' ? '**Symptom:** ' . mb_substr($symptom, 0, 400) . "\n" : '')
. '**Fix:** ' . mb_substr($fix, 0, 1200) . "\n";
$p = vv_ai_incidents_path();
if (!is_dir(dirname($p)) && !@mkdir(dirname($p), 0755, true)) {
return ['ok' => false, 'error' => 'cannot create data dir'];
}
if (!file_exists($p)) {
@file_put_contents($p, "# Incident journal\n\nWhat went wrong, and what actually fixed it."
. " Written by the operator from the Scheduler assistant; fed back when the same"
. " script is diagnosed again.\n");
}
if (@file_put_contents($p, $entry, FILE_APPEND | LOCK_EX) === false) {
return ['ok' => false, 'error' => 'cannot write journal'];
}
return ['ok' => true];
}
// Past entries for one scope, newest first. Capped hard: this rides in the prompt alongside a
// 120-line log and retrieved passages, and the context budget is already the tight thing.
function vv_ai_incidents_for(string $scope, int $max = 4): array {
$p = vv_ai_incidents_path();
if ($scope === '' || !is_file($p)) return [];
$blocks = preg_split('/^## /m', (string)@file_get_contents($p));
$want = strtolower(trim($scope));
$base = strtolower(basename(trim($scope)));
$hits = [];
foreach ($blocks as $b) {
$b = trim($b);
if ($b === '' || !str_contains($b, "\n")) continue;
[$head] = explode("\n", $b, 2);
$head = strtolower($head);
// Match the scope as written, or its basename — the chip may carry a path where an
// older entry carried only the script name.
if (!str_contains($head, $want) && !str_contains($head, $base)) continue;
$hits[] = '## ' . $b;
}
return array_slice(array_reverse($hits), 0, max(1, $max));
}
function vv_ai_job_dir(): string {
if (!is_dir(VV_AI_JOB_DIR)) @mkdir(VV_AI_JOB_DIR, 0700, true);
return VV_AI_JOB_DIR;
+54 -1
View File
@@ -1062,6 +1062,8 @@ Still the same two servers, two households, the same media stack running itself.
placeholder="Ask about what is on screen…"
onkeydown="if(event.key==='Enter'){event.preventDefault();vvAiDockSend();}">
<button class="vv-btn-sm" id="vv-ai-dock-send" onclick="vvAiDockSend()">Ask</button>
<button class="vv-btn-sm" id="vv-ai-dock-fix" onclick="vvAiFixStart()"
style="display:none" title="Record what actually fixed this, against this script">Save fix</button>
<button class="vv-btn-sm" id="vv-ai-dock-hide" onclick="vvAiDockCollapse()"
style="display:none" title="Collapse — the conversation is kept">▾</button>
</div>
@@ -2351,11 +2353,55 @@ function vvAiDockCollapse() {
requestAnimationFrame(vvFitRight);
}
// ── Recording a fix ───────────────────────────────────────────────────────────
// The symptom is taken from the question that was being asked; the fix is typed by the operator.
// The model's diagnosis is deliberately not saved — it is a reading of evidence, and writing a
// hypothesis into institutional memory as settled fact is how a wrong answer outlives the
// incident it came from. What gets remembered is what actually worked.
let vvAiFixMode = false;
let vvAiLastQ = '';
function vvAiFixStart() {
const input = document.getElementById('vv-ai-dock-input');
vvAiFixMode = true;
input.placeholder = 'What actually fixed it? (saved against ' + vvAiScope + ')';
input.value = '';
input.focus();
document.getElementById('vv-ai-dock-send').textContent = 'Save';
document.getElementById('vv-ai-dock-fix').style.display = 'none';
}
function vvAiFixCancel() {
vvAiFixMode = false;
const input = document.getElementById('vv-ai-dock-input');
input.placeholder = 'Ask about what is on screen…';
document.getElementById('vv-ai-dock-send').textContent = 'Ask';
if (vvAiLastQ) document.getElementById('vv-ai-dock-fix').style.display = '';
}
function vvAiFixSave(text) {
fetch('/plugins/varaverk/api/ai.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({ action: 'incident_add', scope: vvAiScope,
symptom: vvAiLastQ, fix: text }),
}).then(r => r.json()).then(d => {
vvAiDockAppend(d.ok
? '<div class="vv-ai-dock-sep">saved against ' + vvEscHtml(vvAiScope)
+ ' — it will be shown next time this is diagnosed</div>'
: '<div class="vv-ai-dock-a vv-ai-dock-err">Could not save: '
+ vvEscHtml(d.error || 'unknown') + '</div>');
}).catch(e => vvAiDockAppend('<div class="vv-ai-dock-a vv-ai-dock-err">Save failed: '
+ vvEscHtml(String(e)) + '</div>'));
vvAiFixCancel();
}
function vvAiDockSend() {
if (vvAiBusy) return;
const input = document.getElementById('vv-ai-dock-input');
const q = input.value.trim();
if (!q) return;
if (vvAiFixMode) { input.value = ''; vvAiFixSave(q); return; }
input.value = '';
vvAiBusy = true;
document.getElementById('vv-ai-dock-send').disabled = true;
@@ -2369,11 +2415,16 @@ function vvAiDockSend() {
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({
action: 'ask', question: q, profile: vvAiProfile, scope: vvAiScope,
think: '0', history: JSON.stringify(vvAiHist),
// Thinking on for diagnosis only. Working out what a log means is reasoning, and it is
// the one thing here worth waiting ~15s for; a lookup like "what does this setting do"
// is not, and inline answers that stall feel broken.
think: vvAiProfile === 'troubleshoot' ? '1' : '0',
history: JSON.stringify(vvAiHist),
}),
}).then(r => r.json()).then(d => {
if (!d.ok || !d.token) return vvAiDockDone(d.error || 'Could not start.', true);
vvAiHist.push({ role: 'user', content: q });
vvAiLastQ = q;
vvAiDockPoll(d.token);
}).catch(e => vvAiDockDone('Request failed: ' + e, true));
}
@@ -2415,6 +2466,8 @@ function vvAiDockDone(text, isErr, sources) {
}
vvAiBusy = false;
document.getElementById('vv-ai-dock-send').disabled = false;
// Offered only once there is something to attach a fix to.
if (!isErr && vvAiLastQ) document.getElementById('vv-ai-dock-fix').style.display = '';
const body = document.getElementById('vv-ai-dock-body');
body.scrollTop = body.scrollHeight;
}