Skip to content

fix(llm-worker): isolate cycle stages so one failure stops discarding the others' work - #1749

Merged
Xore merged 1 commit into
mainfrom
llm-worker-stage-isolation
Aug 23, 2026
Merged

fix(llm-worker): isolate cycle stages so one failure stops discarding the others' work#1749
Xore merged 1 commit into
mainfrom
llm-worker-stage-isolation

Conversation

@Xore

@Xore Xore commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Refs #1748

The structural bug

run_once() called its three stages unguarded. When analyze_daily_report() raised — which it did every cycle from 2026-08-22 — the whole cycle aborted. The sessions and payloads already analysed that cycle were thrown away, and the status document reported nothing but the exception type.

That's what made a broken daily report look like a broken worker, and why LLM_DAILY_REPORT_ENABLED=false was the only workaround: turning the report off was the only way to get the other two pipelines running again.

The three stages share one flaky dependency — a model endpoint — so any of them can fail transiently. This isolates them: a failure lands in stage_errors and the cycle is reported not-ok, but the other stages still run and their results still count.

Only the exception type is recorded, never its message. These exceptions come from a model fed attacker-controlled text, and a message can carry that text back out into the status file. The outer cycle handler already applied that reasoning; this keeps it.

On the grammar failure itself — it no longer reproduces

I tried to reproduce it before fixing it, and could not. Tested against the deployed Ollama:

real DailyReport.model_json_schema(), byte-for-byte what worker.py sends OK
SessionAnalysis, PayloadAnalysis OK
DailyReport variants — no titles, no maxLength, one array, three arrays all OK
prompt sizes 500 / 4,000 / 12,000 / 20,000 chars all OK

The earlier bisect's report that "a summary string plus a single enum reproduces it" also does not hold now — that shape passes, including at maxLength: 2000.

The Ollama container has not restarted since before the failure was first seen (0.32.13, up 6 days), so this is not a version change. The trigger was environmental and could return.

That is precisely the case this PR exists to survive. I'm deliberately not claiming the grammar bug is fixed — only that it is not currently reproducible, and that re-enabling the report can no longer cost the session and payload pipelines anything if it comes back.

Verified

29 tests pass, including four new cases: a failing stage keeps the others' results, a healthy cycle reports no stage_errors, only the exception type is recorded (asserting the attacker-controlled text does not leak into the status), and isolation applies to every stage rather than just the report.

Next

Re-enable LLM_DAILY_REPORT_ENABLED on the live worker once this is deployed, and confirm a real report is written to the llm-analysis index — the one annotation pipeline that has never produced a single real output.

… the others' work

run_once() called its three stages unguarded. When analyze_daily_report()
raised -- which it did every cycle from 2026-08-22, on a schema Ollama's
grammar compiler rejected -- the whole cycle aborted. The sessions and
payloads already analysed that cycle were thrown away, and the status
document reported nothing but the exception type.

That is what made a broken daily report look like a broken worker, and why
LLM_DAILY_REPORT_ENABLED=false was the only workaround available: turning the
report off was the only way to get the other two pipelines running again.

The three stages share one flaky dependency -- a model endpoint -- so any of
them can fail transiently. Each now runs isolated: a failure is recorded in
stage_errors and the cycle is reported not-ok, but the other stages still run
and their results still count.

Only the exception type is recorded, never its message. These exceptions come
from a model fed attacker-controlled text, and a message can carry that text
back out into the status file; the outer cycle handler already applied that
reasoning and this keeps it.

On the grammar failure itself: it no longer reproduces. The real
DailyReport.model_json_schema(), byte-for-byte what worker.py sends, is
accepted by the deployed Ollama 0.32.13 at every realistic prompt size (500
to 20,000 characters), as are SessionAnalysis and PayloadAnalysis and every
trimmed variant of DailyReport. Notably the Ollama container has not been
restarted since before the failure was first seen, so this is not a version
change -- which means the trigger was environmental and could return. That is
the case this change exists to survive: re-enabling the report can no longer
cost the session and payload pipelines anything.

Refs #1748
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@Xore
Xore merged commit cf0418b into main Aug 23, 2026
86 checks passed
@Xore
Xore deleted the llm-worker-stage-isolation branch August 23, 2026 14:22
Xore added a commit that referenced this pull request Aug 23, 2026
…ive-only overlay (#1752)

Captured-data mode was authorized for this fleet on 2026-08-22 (#1660) by
applying docker-compose.captured-data.yml to the running stack by hand. That
change lived only in the running container. It was in no file, so the Arcane
gitops sync -- which materializes llm-worker/docker-compose.yml -- silently
reverted it on the next redeploy.

The result is worse than falling back to synthetic mode. The env vars survive,
so the worker still believes it is in captured-data mode; it simply cannot
reach anything:

    elastic_transport.ConnectionError: Failed to resolve 'elasticsearch'
    llm-worker: cycle failed safely: RuntimeError

Nothing says "the overlay is gone". I hit this deploying #1749 and only caught
it by checking the container after my own redeploy.

docker-compose.yml's header is right that the mounts and networks must not be
folded into it -- #83 owns that authorization and the base file has to stay
safe by default for anyone running it directly. So nothing is folded. Both
files are untouched; `include` composes them at deploy time, and the Arcane
sync's composePath points at this file instead.

That makes the authorization a reviewable artifact rather than undocumented
state in a running container. Deleting this file returns the deployment to
synthetic-only, which is what makes it a record of the decision rather than a
workaround.

Verified against the real files on the homeserver: the merged config keeps
name: llm-worker, carries LLM_ALLOW_CAPTURED_DATA and the payload mounts, and
attaches all three networks including the external honeypot-llm and
honeypot-llm-data that the base file alone does not.

Closes #1751
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant