Stream the answer as it is written, and let the operator stop it

This commit is contained in:
Gmer4Lfe
2026-08-10 20:22:57 -04:00
parent 9c744b34f8
commit eb24e60b8c
3 changed files with 245 additions and 23 deletions
+62 -1
View File
@@ -207,6 +207,62 @@ if ($action === 'memory_set') {
exit;
}
// ── stop ──────────────────────────────────────────────────────────────────────
// Cancels a generation in flight. Only ever signals ONE pid, verified to be the worker for this
// exact job — never a process group. Signalling a group is what took the WebGUI down on
// 2026-08-07, and no group kill is needed here: the worker is a single php process whose only
// child-like thing is an HTTP connection to Ollama, which dies with it.
//
// Whatever was already generated is kept. A turn stopped at 80% is usually stopped because the
// operator has seen enough, not because they want it discarded.
if ($action === 'stop') {
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
$token = trim($_POST['token'] ?? '');
if (vv_ai_job_path($token) === null) {
echo json_encode(['ok' => false, 'error' => 'Invalid token']); exit;
}
$job = vv_ai_job_read($token);
if ($job === null) { echo json_encode(['ok' => false, 'error' => 'No such job']); exit; }
$status = (string)($job['status'] ?? '');
if ($status === 'done' || $status === 'error' || $status === 'stopped') {
echo json_encode(['ok' => true, 'already' => true, 'status' => $status]); exit;
}
$pid = (int)($job['pid'] ?? 0);
// Below 2 is init or nonsense. A pid we cannot verify is a pid we do not signal.
$killed = false;
if ($pid >= 2) {
// Pid reuse is the reason for this: the recorded worker may have exited seconds ago and
// the number been handed to something else entirely. The cmdline must name both this
// worker and this job's own file before anything is signalled.
$cmdline = @file_get_contents("/proc/$pid/cmdline");
$cmdline = $cmdline === false ? '' : str_replace("\0", ' ', $cmdline);
if (strpos($cmdline, 'ai_chat_worker.php') !== false && strpos($cmdline, $token) !== false) {
$killed = @posix_kill($pid, SIGTERM);
// No escalation ladder. The worker holds no lock and writes the job file atomically,
// so there is no cleanup that a delay would protect — and a SIGKILL race could land
// between the temp write and the rename.
}
}
// The job file is rewritten either way. If the pid could not be verified the worker is
// already gone, and the page still needs a terminal state instead of polling to its ceiling.
$job['status'] = 'stopped';
$job['stopped'] = true;
$job['answer'] = trim((string)($job['partial'] ?? $job['answer'] ?? ''));
unset($job['partial']);
@file_put_contents(vv_ai_job_path($token), json_encode($job));
vv_ai_log(sprintf('stop token=%s pid=%d signalled=%s kept=%d chars',
substr($token, 0, 12), $pid, $killed ? 'yes' : 'no', strlen($job['answer'])));
echo json_encode(['ok' => true, 'signalled' => $killed, 'kept' => strlen($job['answer'])]);
exit;
}
// ── clear ─────────────────────────────────────────────────────────────────────
if ($action === 'clear') {
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
@@ -433,7 +489,12 @@ if ($action === 'ask') {
exit;
}
$cmd = 'nohup php ' . escapeshellarg($worker) . ' '
// setsid, not just nohup. nohup detaches from the terminal but leaves the child in the
// caller's process group — php-fpm's. That is the arrangement that took the WebGUI down on
// 2026-08-07 when a Stop signalled a group it did not own. Stop below signals one verified
// pid and never a group, so this is belt and braces, but it also means a php-fpm restart no
// longer takes a running generation with it.
$cmd = 'setsid nohup php ' . escapeshellarg($worker) . ' '
. escapeshellarg($jobFile) . ' '
. escapeshellarg($question) . ' '
. escapeshellarg(json_encode($clean)) . ' '