Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist-renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ProofForge</title>
<script type="module" crossorigin src="./js/main-2TeVSSYI.js"></script>
<script type="module" crossorigin src="./js/main-I-j1AS1W.js"></script>
<link rel="modulepreload" crossorigin href="./js/vendor-misc-DYLXRpC5.js">
<link rel="modulepreload" crossorigin href="./js/vendor-state-B3vIvpM5.js">
<link rel="modulepreload" crossorigin href="./js/vendor-react-C1Vrmh6-.js">
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# PR #165 — LLM Health Snapshot Unify and Session-Lock Repair Receipt

**Date:** 2026-06-26
**Type:** product / runtime bug fix
**Branch:** `product/llm-health-snapshot-unify`
**Master at branch:** `73f2cf5` (post-PR #164)

---

## Root causes fixed

### 1. Local Ollama health check tested at api.openai.com

`checkLocalOllama` in `src/core/systemHealthCheck.js` used `settings.ollamaBaseUrl`
as its base URL. The `ollamaBaseUrl` field is a multipurpose "base URL" key that
stores the endpoint for whatever provider is currently selected. When the previous
provider was OpenAI, `ollamaBaseUrl` held `https://api.openai.com`. The function
then tested *that* URL while labeling the check "Local Ollama," producing the
confusing result:

```
Local Ollama
Ollama not detected at https://api.openai.com
request failed with status 404
```

**Fix:** When `settings.provider !== 'ollama'`, always use `http://127.0.0.1:11434`
regardless of `ollamaBaseUrl`. When the active provider *is* Ollama, `ollamaBaseUrl`
is used as-is (it holds the correct local endpoint after a provider switch).

### 2. Hosted policy check reported "openai selected" when route was ollama

`checkHostedPolicy` read `settings.provider` directly. If there was an active
connection profile with `provider: 'openai'`, and `settings.provider` was also
`'openai'` (stale settings), the check reported "Provider openai selected but
Hosted Provider Access is disabled" even though the UI showed ollama/llama3.

**Fix:** Prefer the active connection profile's provider over the raw
`settings.provider`. Falls back to `settings.provider` if no active profile exists.

### 3. "Runtime Fatal" from update verification watchdog

`updateVerificationCheck.ts` returned `severity: "critical"` for any non-verified
update lane state. In App.jsx, any unacknowledged `critical` watchdog alert triggers
`watchdogStatus = 'fatal'`, which surfaces as "Runtime Fatal" in the trust strip.
An update verification issue (e.g. signatures not yet checked) is not a runtime
process crash and should not collapse the entire runtime health to fatal.

**Fix:** Downgrade update verification severity from `"critical"` to `"degraded"`.
The comment documents that "critical" is reserved for runtime process crashes,
data corruption, or security breaches.

### 4. Locked sessions returned confusing vault/NodeChain responses in chat

`executeSignal` checked `auditOnly` (Auditor tier) but did not check whether the
active session was locked. A locked session could still reach the LLM, but the
response was filtered through NodeChain/vault guards, producing internal messages
like "NodeChain prompted a vault save" instead of the expected model response.
The user had no indication that the session lock was blocking normal chat.

**Fix:** Before freeform chat (non-command input), check `isSessionUnlocked(workflowId)`.
If the session is locked, append a clear message:
```
Session "Workflow_RYV9B" is locked. Unlock it to send chat messages (proof commands still work).
```
Commands (`/proof`, `/roi`, etc.) are exempt because they are local operations,
not LLM calls.

---

## Files changed

| File | Change |
|---|---|
| `src/core/systemHealthCheck.js` | `checkLocalOllama`: force local URL when provider != ollama. `checkHostedPolicy`: prefer active profile provider |
| `src/renderer/src/runtime/watchdog/checks/updateVerificationCheck.ts` | severity: `"critical"` → `"degraded"` |
| `src/renderer/src/App.jsx` | `executeSignal`: session lock check blocks freeform chat with clear message |

---

## What was NOT changed

- Watchdog severity taxonomy — update verification was the only `critical` source
from the watchdog; the rest (vault, relay, provider) were already `warning`/`degraded`
- The `vaultHealthCheck` `"warning"` severity — vault locked is a session state, not
a health failure, but changing its severity class touches more components
- The `ollamaBaseUrl` field name — it's a universal base URL field, not Ollama-specific;
renaming it is out of scope for a targeted repair PR

---

## Runtime proof (all PASS)

| Gate | Result |
|---|---|
| `npm run build:renderer` | PASS |
| `npm run test:local:ops` | PASS (13/13 + spine + bridge-diagnosis) |
| `npm run smoke:operator` | PASS (exit 0) |
| `npm audit --omit=dev --audit-level=high` | 0 vulnerabilities |
22 changes: 17 additions & 5 deletions src/core/systemHealthCheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,14 @@ async function checkActiveLlmBridge(deps = {}) {

async function checkLocalOllama(deps = {}, options = {}) {
const settings = deps.settings && typeof deps.settings === "object" ? deps.settings : {};
const baseUrl = String(
settings.ollamaBaseUrl
|| "http://127.0.0.1:11434"
).trim();
// ollamaBaseUrl stores the base URL for whatever provider is currently selected.
// When the active provider is not Ollama, it may hold a hosted URL (e.g. api.openai.com)
// which has no relationship to where the local Ollama server runs.
// Always fall back to the standard local endpoint when Ollama is not the active provider.
const activeProvider = String(settings.provider || "ollama");
const baseUrl = activeProvider === "ollama"
? String(settings.ollamaBaseUrl || "http://127.0.0.1:11434").trim()
: "http://127.0.0.1:11434";
const started = Date.now();
const probe = new LLMService({
provider: "ollama",
Expand Down Expand Up @@ -441,7 +445,15 @@ async function checkAuditChain(deps = {}) {
async function checkHostedPolicy(deps = {}) {
const settings = deps.settings || {};
const allowRemote = Boolean(settings.allowRemoteBridge);
const provider = String(settings.provider || "ollama");
// Prefer the active connection profile's provider over the raw settings.provider.
// The raw provider field can be stale when a profile was switched but not yet saved.
const profiles = Array.isArray(settings.connectionProfiles) ? settings.connectionProfiles : [];
const activeProfile = profiles.find((p) =>
String(p?.id || '') === String(settings.activeProfileId || '')
) || profiles[0] || null;
const profileProvider = activeProfile ? String(activeProfile.provider || '').trim().toLowerCase() : '';
const rawProvider = String(settings.provider || "ollama");
const provider = profileProvider || rawProvider;
const remote = provider !== "ollama";
if (remote && !allowRemote) {
return buildCheck({
Expand Down
11 changes: 11 additions & 0 deletions src/renderer/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2552,6 +2552,17 @@ function App() {
}
}

// Freeform chat is blocked when the session is locked. Commands (/proof, etc.)
// pass through because they are local proof operations, not LLM calls.
if (!command.startsWith('/') && workflowId && workflowId !== QUICKSTART_SESSION && !isSessionUnlocked(workflowId)) {
setIsThinking(false);
appendChat({
role: 'kernel',
content: `Session "${workflowId}" is locked. Unlock it to send chat messages (proof commands still work).`,
});
return;
}

if (command.startsWith('/')) {
setIsThinking(false);
if (command === '/clear' || command === '/purge' || command === '/reset') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export function updateVerificationCheck(input: UpdateVerificationInput) {
if (!staged && signatureState === "unknown") return null;
return {
source: "update-lane",
severity: "critical",
// degraded, not critical — update lane issues are deploy-time, not runtime-crash.
// Reserve "critical" for runtime process crashes, data corruption, or security breaches.
severity: "degraded",
message: `Update verification is ${signatureState}.`,
suggestedAction: "Freeze update apply lane until signature/hash verification passes.",
};
Expand Down
Loading