Add an incident journal the troubleshooter reads back, and thinking for diagnosis
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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'] ?? '') : '';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user