Give repair its own profile, reading what was said before and what fixed it before

Conf writing is granted to this profile alone and it is not offered as a button — making it
selectable would put a write one click from any question. Its prompt leads on what it does
not decide, because the resolver picks the key and the probe picks the value.
This commit is contained in:
Gmer4Lfe
2026-08-09 20:49:43 -04:00
parent 4ab3387db4
commit 9c150532fc
3 changed files with 134 additions and 0 deletions
+49
View File
@@ -446,6 +446,55 @@ if ($profile === 'chat') {
. "knowledge freely — Linux, scripting, hardware, whatever comes up. The restriction "
. "is only about the specifics of THIS installation.\n\n";
} elseif ($profile === 'repair') {
// The only profile that can change a setting, so the prompt's job is mostly to stop it
// believing that it decides anything. It does not: the resolver picks the key, the probe
// picks the value, and the operator picks whether to write. What the model contributes is
// the explanation and the conversation — the parts where being wrong costs a sentence.
//
// Prior context first, because the failure this profile is most prone to is confidently
// re-deriving something already settled: proposing a fix that was tried and did not work,
// or reading a term the operator has already corrected once.
require_once dirname(__DIR__) . '/include/ai_repair.php';
$priorContext = vv_ai_repair_context();
if ($priorContext !== '') $attached['prior'] = 'phrasebook + closed findings';
$system = "You are helping the operator of a Varaverk server deal with a finding — something "
. "on this machine that is misconfigured or is reporting a problem.\n\n"
. "WHAT YOU DO NOT DECIDE\n"
. "You do not choose which setting is involved: that was resolved from the conf "
. "before you were asked. You do not choose what value to write: a probe either got "
. "an answer from a candidate or it did not, and only a value that answered can be "
. "written. You do not decide whether to apply anything — the operator does.\n\n"
. "So never say you have changed, set, fixed or updated anything. If a change was "
. "applied you will have been told so; otherwise it has not happened yet.\n\n"
. "TOGGLES ARE NEVER YOURS TO FLIP\n"
. "Whether something should be switched on is a decision about what the operator "
. "wants, not a fact you can discover. Offer it, explain the consequence, and wait.\n\n"
. "WHEN THE VALUE CANNOT BE WORKED OUT HERE\n"
. "Some things cannot be derived from this machine at all — an API key most of all. "
. "Say so plainly and walk the operator through getting it, step by step, naming the "
. "screen and the field. Then ask them to paste it. That is a normal outcome and not "
. "a failure; pretending to have found it is the failure.\n\n"
. "HOW TO ANSWER\n"
. "Lead with what is wrong and what it stops working. Quote the evidence on the "
. "finding. Then give the operator their choices in plain terms: apply the proposed "
. "value, acknowledge it as intended, or leave it. Keep it short — they are reading "
. "this to make one decision.\n\n"
. "Say plainly when you do not know. A wrong cause sends someone to fix the wrong "
. "thing, which is worse than saying the finding does not explain itself.\n\n"
. ($priorContext !== ''
? "WHAT HAS ALREADY HAPPENED HERE\n"
. "Use this before reasoning from scratch. If a term below was corrected, use the "
. "corrected meaning without being told again. If a similar finding was closed "
. "before, say what ended it last time.\n\n" . $priorContext . "\n\n"
: "");
} elseif ($profile === 'troubleshoot') {
// Different inputs and a different refusal rule from the assistant, which is what earns it a
// profile of its own. The assistant's contract is "answer only from the passages, refuse if
+25
View File
@@ -109,6 +109,26 @@ const VV_AI_PROFILES_DEF = [
'ui' => true,
'caps' => ['retrieve', 'health', '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.
//
// It is entered by opening a finding, the way troubleshoot was originally entered by opening
// a log — a repair conversation with nothing to repair is a contract with no subject. That is
// not the only reason though: capabilities are granted per profile, so making this selectable
// would put conf_write one click away from any question at all. The narrow grant is the point.
//
// Reads two records of what has happened before. The phrasebook holds what the operator calls
// things and, more usefully, what they have had to correct; closed findings hold what actually
// fixed a fault last time. Between them the profile starts a conversation already knowing the
// operator's vocabulary and this installation's history, instead of relearning both each time.
'repair' => [
'label' => 'Repair',
'short' => 'Repair',
'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'],
],
];
// Documentation only — nothing branches on it. Kept beside the table because a capability name
@@ -123,6 +143,11 @@ const VV_AI_CAP_MEANING = [
'conf_lookup' => 'deterministic "where does this conf key actually live" lookup',
'file_bugs' => 'may file a bug report against Varaverk itself',
'code_scan' => 'destructive-operation scan of generated shell',
'conf_write' => 'may change a setting — through the guarded write path, never directly',
'probe' => 'may test a candidate address before anything is written to conf',
'file_findings'=> 'may record, acknowledge and close findings about this installation',
'phrasebook' => 'what the operator calls things, and what they have corrected before',
'past_fixes' => 'findings already closed — what fixed this last time',
];
function vv_ai_profiles(): array { return VV_AI_PROFILES_DEF; }
+60
View File
@@ -915,6 +915,66 @@ function vv_ai_phrase_lookup(string $term, int $minSeen = 3): ?array {
return vv_ai_phrase_aliases($minSeen)[strtolower(trim($term))] ?? null;
}
// ── What the repair profile is given before it answers ───────────────────────────────────────
// Two records of what has already happened, assembled as plain text for the prompt.
//
// The phrasebook half is the operator's vocabulary — and the corrections matter more than the
// settled terms, because a term that has been corrected is one the assistant has already got
// wrong once and would otherwise get wrong again.
//
// The closed-findings half is this installation's history: the same fault, and what actually
// ended it. "Emby stopped answering last month and the address had changed" is worth more at the
// start of a repair conversation than any amount of reasoning from first principles.
//
// Bounded hard. This goes into a 16k context that retrieval and a log tail are also competing
// for, and an unbounded history would crowd out the evidence for the fault actually being
// discussed.
function vv_ai_repair_context(int $maxAliases = 20, int $maxFixes = 8): string {
$out = [];
$aliases = vv_ai_phrase_aliases();
if ($aliases) {
$lines = [];
foreach (array_slice($aliases, 0, $maxAliases, true) as $term => $a) {
$lines[] = ' "' . $term . '" means ' . $a['target'];
}
$out[] = "What the operator calls things:\n" . implode("\n", $lines);
}
$unsettled = vv_ai_phrase_unsettled();
if ($unsettled) {
$lines = [];
foreach (array_slice($unsettled, 0, $maxAliases, true) as $term => $c) {
$last = end($c);
$lines[] = ' "' . $term . '" was read as ' . ($last['took_it_as'] ?: '?')
. ' and meant ' . ($last['meant'] ?: '?');
}
$out[] = "Corrected before — do not repeat these:\n" . implode("\n", $lines);
}
$closed = vv_ai_findings_list(['fixed', 'resolved']);
if ($closed) {
$lines = [];
foreach (array_slice($closed, 0, $maxFixes) as $f) {
$lines[] = ' ' . ($f['subject'] ?? '?') . ' / ' . ($f['ref'] ?? $f['conf_key'] ?? '?')
. ' — ' . ($f['state'] ?? '?')
. (!empty($f['note']) ? ': ' . mb_substr((string)$f['note'], 0, 140) : '');
}
$out[] = "Already dealt with on this host:\n" . implode("\n", $lines);
}
$spellings = vv_ai_spellings();
if ($spellings) {
$lines = [];
foreach (array_slice($spellings, 0, $maxAliases, true) as $typo => $meant) {
$lines[] = ' "' . $typo . '" = "' . $meant . '"';
}
$out[] = "The operator types quickly; known shorthand:\n" . implode("\n", $lines);
}
return implode("\n\n", $out);
}
// ── Resolving what the operator named ────────────────────────────────────────────────────────
// "turn off the zfs scrub", "change the emby api key" — a phrase in, an exact target out, or an
// honest refusal with the candidates that were considered.