Skip to content

ingest: bounded parallel ingest (--jobs N), on a context-local wiki redirect - #129

Merged
MarkusNeusinger merged 12 commits into
mainfrom
claude/audit-2026-07-workpackets-zwr83s
Jul 25, 2026
Merged

ingest: bounded parallel ingest (--jobs N), on a context-local wiki redirect#129
MarkusNeusinger merged 12 commits into
mainfrom
claude/audit-2026-07-workpackets-zwr83s

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Jul 25, 2026

Copy link
Copy Markdown
Owner

What

The 2026-07 audit's backlog #11 — the last open item in its ranked table — shipped in the two steps the audit itself scoped: first the unblock (finding 1.2.5), then the feature.

Unblock. The per-source staging redirect assigned config.WIKI_DIR/INDEX_PATH/LOG_PATH/MANIFEST_PATH and os.environ["CITADEL_WIKI_DIR"] in place, so one process could only ever be inside one redirect. The wiki path is now read through accessors (config.wiki_dir(), index_path(), sources_index_path(), log_path(), manifest_path(), failures_path()) backed by a ContextVar — per-thread by construction, so N workers each hold their own redirect while the main thread still sees the live wiki. Unset, the accessors return the module attributes verbatim, so every read path, every CLI command, the MCP server, and every test that monkeypatches the layout behave exactly as before. The session's child processes get their staging wiki through an explicit per-spawn env (config.child_env()) instead of a global mutation.

Feature. citadel ingest --jobs N / -j (env CITADEL_JOBS, default 1 = the serial behavior, line for line) folds N sources in concurrently, each on its own staging copy — the isolation primitive was already there. Every guarantee is unmoved: one promote per source, all-or-nothing, nothing partial on the live wiki, the manifest still saved per completed source. What is shared is serialized rather than raced:

  • One lock guards the only two moments that touch the live wiki — the clone and the promote. The minutes-long sessions run entirely outside it. (The clone's base hashes are taken off the fresh staging tree, so the lock covers the copy alone and the wiki is read once, not twice.)
  • The promote applies only its own delta, decided against its clone: a staging copy also holds untouched copies of every page the source did not write, so judging by "differs from live" would let a promote revert a page a concurrent source had just rewritten.
  • The prune is base-aware: only what this source's own base had and its staging lacks, so a concurrent source's new page is never deleted as "the agent removed it".
  • A promote whose pages have moved since the clone is refused before it writes a byte (_ConcurrentChange), and that source is re-run serially at the end of its group — where it sees the winner's page and merges into it, i.e. the result a serial run would have produced. Those re-runs are surfaced on the report (Re-run serially), because they are the one place parallelism costs a session serial ingest would not have spent.
  • A chunked source's resume checkpoint is measured against its own clone too — see the review section below; this is the one place where the new concurrency could actually damage a wiki, and it did until it was found.
  • Report / manifest / failures bookkeeping stays on the main thread (workers only plan, stage, run, promote), so those writes are as single-threaded as before, and the progress callback is serialized so a caller's callback is never invoked concurrently. An interrupt cancels queued sources, lets in-flight ones roll themselves back, and still records work already promoted.

The default stays 1 on purpose: the real cost is cross-linking, not safety — concurrent sessions cannot see each other's new pages, so they link less richly and can duplicate a topic (curate is the designed cleanup). curate itself remains serial by construction (its clusters share pages by definition, so nearly every pair would race); refresh inherits CITADEL_JOBS, since it drives ingest.

Three defects the parallel path exposed are fixed at the root: the promote's leftover-temp sweep deleted hidden *.citadeltmp files — another writer's in-flight atomic manifest/failures save — config.atomic_write_text's temp name was unique per PID but not per thread, and the transcript log's sequence counter was bumped without a lock while its own comment promised concurrent files never collide.

Docs: CITADEL_JOBS in docs/configuration.md + the .env template, a "working off a big backlog" recipe in docs/recipes.md, CHANGELOG, CLAUDE.md (architecture + the read-the-wiki-through-the-accessor convention), and the audit itself — #11 ticked off with what shipped and what moved during implementation, finding 1.2.5 / § 1.5 "strictly serial" / § 3.3 annotated, plus a state-of-the-backlog note (all 15 ranked items done; where the still-open work lives).

What review found (worth reading before merging)

An adversarial cross-review by a second model, run specifically against the concurrency, found the one path where this could corrupt a wiki — not in the conflict detection, but where the new parallelism meets the existing resume checkpoints of a chunked source. Both manifestations were reproduced offline before the fix and are now pinned (tests/test_ingest_parallel_resume.py):

  • _checkpoint_delta computed "what promoting staging onto live would do". Under --jobs the live wiki drifts between a source's clone and its checkpoint, so a page a concurrent source had created read as a deletion this source made, and one it had rewritten read as this source's change carrying stale bytes. A checkpoint is durable: a later run — even a strictly serial one — replayed that delta and pruned a fully-ingested source's page off the live wiki, with no conflict, no error and no delete session.
  • resume.save compounded it by recording the base state at save time (after the concurrent promote), so replay's guard verified "live unchanged since I saved" while the delta embodied "live as of my clone" — the wrong invariant, which a concurrent promote passes straight through. A raced chunked source then re-ran with zero sessions and promoted its stale copy over the winner's page, silently violating the documented merge contract.

Both now derive from the source's own clone snapshot — exactly the rule the base-aware promote uses — so a page another source touched fails the guard and the checkpoint is dropped for a full restart.

Copilot's rounds contributed the lock-window narrowing (hash the clone, not the live wiki), the serialized progress callback, the missing resumed_note on the conflict path (now with one owner, _record_spend, so a future branch cannot forget it), and two hygiene fixes (a dropped word in the --jobs help text, a leaked file handle in a test). One Copilot finding was a false positive — future.result() on a cancelled future cannot abort the run, since a future is only cancelled after an interrupt is recorded and the first interrupt wins — but the loop now skips cancelled futures explicitly rather than relying on that reasoning.

Testing

  • uv run pytest -q — 1216 passed, 1 skipped
  • uv run ruff check . / uv run ruff format --check . — clean
  • CITADEL_WORKSPACE=corpora/beverages uv run python -m citadel lint — OK
  • 20 new offline tests (tests/test_ingest_parallel.py, tests/test_ingest_parallel_resume.py) drive the concurrency for real: the fake sessions synchronize on a threading.Barrier, so a test requiring two sources in flight at once cannot pass on a serial run. Counter-checked by forcing the driver serial — the concurrency tests fail exactly as intended. Each safety property is pinned to the failure it prevents (a concurrent source's pages surviving a promote, only-my-own-delta, the serial re-run and its failure path, the checkpoint that must not bank another source's page as a deletion, the raced chunked source that must really re-run, the interrupt keeping promoted work, the hidden-temp sweep, the never-concurrent progress callback).
  • verify-corpus was not run: the claude CLI in this container is not authenticated (a live 4-source --jobs 4 run failed every session with Authentication error). That run did still exercise the parallel driver end-to-end with real subprocesses — four sessions in flight simultaneously, per-source failure recording, finalize. The rules tree is untouched (no rules_version change) and the serial path is unchanged, so wiki quality for the default configuration cannot have moved; a corpus grade of a --jobs > 1 run is worth doing on a machine with a logged-in CLI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt

claude added 3 commits July 25, 2026 11:53
The 2026-07 audit's finding 1.2.5 (the blocker under backlog #11, `--jobs N`):
ingest's per-source staging redirect ASSIGNED config.WIKI_DIR/INDEX_PATH/
LOG_PATH/MANIFEST_PATH and os.environ["CITADEL_WIKI_DIR"] in place, so one
process could only ever be inside one redirect — two sources could never be
staged at once.

The wiki path is now read through accessors (config.wiki_dir()/index_path()/
sources_index_path()/log_path()/manifest_path()/failures_path()) backed by a
ContextVar. A ContextVar is per-thread by construction, so N workers can each
hold their own staging redirect while the main thread still sees the live wiki.
Unset — every read path, every CLI command, the MCP server — the accessors
return the module attributes verbatim, so the process-wide layout (and every
test that monkeypatches it) behaves exactly as before.

The child processes a session spawns get the staging wiki through an explicit
per-spawn env (config.child_env()) rather than a global os.environ assignment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Backlog #11 of the 2026-07 audit, unblocked by the context-local wiki
redirect: N sources are folded in concurrently, each still against its own
staging copy and its own all-or-nothing promote.

What is shared is serialized, not raced:

* one lock guards every touch of the LIVE wiki — the clone (taken together
  with a hash snapshot of what was cloned) and the promote. Sessions, the
  minutes-long part, run entirely outside it.
* the promote is base-aware under --jobs: it prunes only what its OWN base
  had and its staging lacks (so a concurrent source's new page is never
  deleted as "the agent removed it"), and refuses — before writing a byte —
  if any path it would touch has moved since the clone.
* a refused promote is not a failure: the source is re-run serially at the
  end of its group, where it sees the winner's page and merges into it, the
  result a serial run would have produced. Reported as `raced`, since it is
  the one place parallelism costs a session a serial run would not have.
* report/manifest/failures bookkeeping stays on the main thread, so those
  writes are as single-threaded as before and the manifest is still saved per
  completed source. An interrupt cancels queued sources and still records the
  work already promoted.

Default is 1 — unchanged serial behavior, line for line. The trade-off is
cross-linking (concurrent sessions cannot see each other's new pages), so the
knob is documented as a throughput trade, not a free win.

Also fixes a defect the parallel path exposed: the promote's leftover-temp
sweep deleted HIDDEN *.citadeltmp files, i.e. another writer's in-flight
atomic manifest/failures save; and atomic_write_text's temp name is now
unique per thread, not just per pid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Self-review fix on top of --jobs N: which pages a source touched is decided
against its OWN clone, not against the current live wiki. A staging copy also
holds untouched copies of every page the source did not write, so judging by
"differs from live" made a page a concurrent source had just rewritten look
like this source's change — safe (the base check refused the promote) but
wrong: it forced a needless serial re-run, and the intent behind it was to
copy a stale page over newer work. Pinned by a test where one source rewrites
a pre-existing page while another only writes its own.

Docs: CITADEL_JOBS in docs/configuration.md + the .env template, a
"working off a big backlog" recipe in docs/recipes.md, the CHANGELOG entry,
the CLAUDE.md architecture + convention notes (the wiki path is read through
config.wiki_dir(), never config.WIKI_DIR), and the audit itself — backlog #11
ticked off with what shipped and what moved during implementation, plus
finding 1.2.5, the § 1.5 "strictly serial" gap and the § 3.3 parallelism
bullet annotated, and a state-of-the-backlog note recording that all 15
ranked items are now done and where the still-open work lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Copilot AI review requested due to automatic review settings July 25, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds bounded parallelism to citadel ingest via --jobs N / CITADEL_JOBS, enabled by refactoring the per-source staging redirect from process-global mutation (config.WIKI_DIR / os.environ) to a context-local (ContextVar) accessor model (config.wiki_dir() and related path accessors). This preserves ingest’s staging/promote safety guarantees while allowing multiple sources to be folded concurrently in one process.

Changes:

  • Introduces citadel ingest --jobs N (and CITADEL_JOBS) to run multiple source sessions concurrently, with serialized, base-aware promotion and serial re-runs on detected conflicts.
  • Reworks wiki-path consumption across the codebase to go through redirect-aware accessors (config.wiki_dir(), index_path(), etc.) and passes the staging wiki path to child processes via config.child_env().
  • Adds a comprehensive offline concurrency test suite and updates docs/config/templates/changelog to describe the new knob and its trade-offs.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_usage_accounting.py Updates test to use config.wiki_dir() for redirect-aware wiki path access.
tests/test_runlock.py Updates test to use config.wiki_dir() when referencing the live wiki.
tests/test_pagecache.py Updates tests to use config.wiki_dir() and adjusts docstrings to match redirect-aware semantics.
tests/test_netdrive.py Uses config.wiki_dir() in netdrive layout tests to respect redirects/out-of-repo layouts.
tests/test_ingest_staging.py Updates staging assertions to use config.wiki_dir() and reflect redirect behavior.
tests/test_ingest_resume.py Updates resume tests to write/remove pages under config.wiki_dir().
tests/test_ingest_reconcile_delete.py Updates deletion reconciliation test to use config.wiki_dir().
tests/test_ingest_progress.py Extends progress event expectations to include the jobs field.
tests/test_ingest_parallel.py New: offline tests that prove true concurrency (Barrier) and pin safety properties under --jobs.
tests/test_ingest_core.py Updates core ingest tests to use redirect-aware wiki path access.
tests/test_ingest_chunking.py Updates chunking tests to assert against config.wiki_dir() in staging.
tests/test_curate.py Updates curate test to read log via config.log_path().
tests/conftest.py Updates fixtures and helpers to read/write wiki paths via accessors at call time.
docs/recipes.md Documents a “working off a big backlog” recipe using citadel ingest --jobs N.
docs/configuration.md Adds CITADEL_JOBS documentation and explains safety vs. cross-linking trade-off.
docs/audit-2026-07.md Marks audit backlog #11 complete and records shipped design details.
CLAUDE.md Updates architecture/conventions to require wiki access via accessors and documents bounded parallelism.
citadel/wikigit.py Switches from config.WIKI_DIR to config.wiki_dir() for redirect-aware wiki location.
citadel/viewer/init.py Uses config.wiki_dir() for viewer naming, source-link calculation, and output path defaults.
citadel/transcribe.py Derives transcript cache sibling directory from config.wiki_dir().
citadel/templates/env.example Adds commented CITADEL_JOBS section and throughput guidance.
citadel/store_core.py Makes read/write paths redirect-aware (config.wiki_dir(), config.index_path(), etc.).
citadel/server.py Uses config.wiki_dir() for safe on-disk presence checks.
citadel/runlock.py Derives lock file location from config.wiki_dir() parent.
citadel/resume.py Derives resume cache dir from config.wiki_dir() parent.
citadel/progress.py Adds jobs to on_start and introduces on_source_retry for conflict-triggered serial re-runs.
citadel/pdftext.py Derives PDF text cache dir from config.wiki_dir() parent.
citadel/pagecache.py Updates documentation references to match redirect-aware wiki semantics.
citadel/manifest.py Uses config.manifest_path() for redirect-aware manifest IO and warnings.
citadel/llm.py Ensures LLM subprocesses receive the staging wiki via env=config.child_env() and uses config.wiki_dir() in prompt building.
citadel/linkgraph.py Makes link rewrite and raw-reference rewrite write targets redirect-aware (config.wiki_dir()).
citadel/ingest.py Implements --jobs concurrency, base-aware promotion, conflict detection + serial re-run, and thread-safe staging naming.
citadel/grammar.py Uses config.wiki_dir() when resolving/normalizing internal link paths.
citadel/failures.py Uses config.failures_path() for redirect-aware failures catalog IO.
citadel/doctor.py Uses redirect-aware manifest/wiki paths (e.g., config.manifest_path(), config.wiki_dir()).
citadel/curate.py Uses config.wiki_dir() when walking/reading wiki pages and selecting pages.
citadel/config.py Introduces wiki accessors + ContextVar redirect, child_env(), CITADEL_JOBS, and thread-unique temp naming for atomic writes.
citadel/cli.py Adds --jobs/-j flag, validates usage, and threads the value through to ingest.ingest().
citadel/catalogs.py Writes generated catalogs/indexes using redirect-aware wiki paths.
CHANGELOG.md Records the new bounded parallel ingest feature and the context-local redirect change.

Comment thread citadel/ingest.py
Copilot review round 1. clone() held the live-wiki lock across BOTH the
copytree and a full hash walk of the live wiki — two reads of the corpus and a
serialization window twice as long as it needs to be, on the exact path
--jobs N exists to speed up.

The staging tree is a byte-exact copy of what was just cloned, so the base can
be hashed off it, after the lock is released: identical hashes, one read of the
wiki instead of two, and every other worker's clone/promote unblocked sooner.
It still has to happen before anything mutates staging — a resume replay writes
into it — which the comment now says explicitly.

Pinned by a test that a clone hashes identically to what it copied, over the
whole file set _content_files considers (nested folders and non-.md files
included).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Copilot AI review requested due to automatic review settings July 25, 2026 12:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated no new comments.

Adversarial cross-review (second model) of the --jobs N concurrency found a
real data-loss path where the new parallel machinery meets the existing resume
checkpoints of a CHUNKED source. Both manifestations were reproduced offline
before fixing, and the tests are committed.

_checkpoint_delta computed "what promoting staging onto LIVE would do". Under
--jobs the live wiki drifts between a source's clone and its checkpoint, so:

* a page a CONCURRENT source created read as "in live, not in my staging" —
  banked as a deletion this source had made. A checkpoint is durable, so a
  later run — even a strictly serial one — replayed it and pruned a
  fully-ingested source's page off the live wiki: no conflict, no error, no
  delete session, just a page gone.
* a page a concurrent source rewrote read as this source's change, carrying
  the clone's stale bytes.

resume.save compounded it by recording the base state at SAVE time (after the
concurrent promote), so replay's guard verified "live unchanged since I saved"
while the delta embodied "live as of my clone" — the wrong invariant, which a
concurrent promote passes straight through.

The delta and the recorded base state now both come from the source's own
clone snapshot — exactly the rule the base-aware promote already uses. A page
another source touched fails the guard and the checkpoint is dropped for a
full restart. That also fixes the second manifestation: a raced chunked source
whose checkpoint had completed == total was re-run with ZERO sessions and
promoted its stale copy over the winner's page, silently violating the
documented "re-run serially and merge into the winner" contract.

Also locks llm's transcript sequence counter, whose own comment promised
concurrent files never collide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Copilot AI review requested due to automatic review settings July 25, 2026 16:07
claude added 2 commits July 25, 2026 16:07
The durable half of the previous commit: what the adversarial cross-review
found, why a checkpoint measured against a moving live wiki outlives the run
that created it, and the three root-cause fixes the parallel path exposed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

citadel/ingest.py:2459

  • emit() can be called concurrently from worker threads under --jobs > 1 (e.g. source_start is emitted inside _attempt_source). If the progress callback has any internal state (including ConsoleProgress’s _label/spinner fields), concurrent calls can race and produce incorrect output or intermittent errors (which are then silently swallowed). Serializing progress-callback invocations with a simple lock keeps behavior deterministic while still allowing the sessions themselves to run in parallel.
    def emit(event: str, **data) -> None:
        if progress is not None:
            try:
                progress(event, data)
            except Exception:  # noqa: BLE001 - progress must never break ingest
                pass

Comment thread citadel/ingest.py
Copilot AI review requested due to automatic review settings July 25, 2026 16:11
…spend

Copilot review round 2, both points genuine.

emit() now fires from worker threads, so the callback — whatever a caller
passed — is serialized behind a lock. "Your progress callback is never invoked
concurrently" becomes a property of the API instead of something each caller
has to discover; the console reporter was safe either way (locked writes,
spinner off when jobs > 1), but the contract must not rest on that reasoning
holding for every future callback. It costs nothing: emit fires a handful of
times per source, around sessions that take minutes.

The conflict path bypassed _record_source_run and so dropped the outcome's
resumed_note, even though the checkpoint replay had already happened — the
codebase's own rule is that a restored checkpoint is recorded whatever the
verdict. Both facts that hold regardless of verdict (what an attempt cost, what
it reused) now live in one owner, _record_spend, called from the normal path
and from the conflict path, so the next branch cannot forget one of them again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated 3 comments.

Comment thread citadel/ingest.py
Comment thread citadel/cli.py Outdated
Comment thread tests/test_ingest_parallel.py Outdated
Copilot AI review requested due to automatic review settings July 25, 2026 16:13
…a test fd

- _run_source_jobs_parallel's main loop now skips a cancelled future instead of
  calling result() on it. The described failure (the run aborting with an
  unexpected CancelledError instead of the original interrupt) cannot actually
  happen — a future is only ever cancelled after an interrupt is recorded, and
  the first interrupt wins — but the loop relied on that reasoning rather than
  saying it. The drain loop already skipped cancelled futures; now both do.
- `citadel ingest --jobs` help text dropped a stray word ("Fold up to N sources
  in CONCURRENTLY").
- The progress test opened a source file as its output stream and never closed
  it; io.StringIO is what it wanted in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

tests/test_ingest_parallel.py:413

  • This test opens a file handle for ConsoleProgress(stream=...) without closing it. Using a context manager avoids leaking descriptors (and keeps pytest warning configurations from turning this into a failure).

Copilot AI review requested due to automatic review settings July 25, 2026 16:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 41 changed files in this pull request and generated no new comments.

refresh already honored CITADEL_JOBS — it drives ingest — but had no flag of
its own, which was the weaker half of keeping the new CLI surface small: a
refresh slice is ordered by LAST-CHECKED time rather than by topic, so its
sources are typically unrelated, which is exactly the case --jobs exists for.

Same knob, same trade-off, same refusal below 1 (exit 2, mirroring ingest);
refresh only chooses which sources to hand ingest. Docs, help text and the
CHANGELOG say so in one place each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Copilot AI review requested due to automatic review settings July 25, 2026 16:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 1 comment.

Comment thread tests/conftest.py Outdated
Copilot round 4. The refactor moved five of these field comments to the
accessor form and left sources_index_path on the old attribute name, so the
dataclass documented two conventions at once.

Made consistent, plus the sentence that makes the annotation mean something:
the fixture WIRES the module attributes, the fields are annotated with the
ACCESSOR because that is how code reads them — and inside a fake session only
the accessor sees the staging copy the session is supposed to edit, while the
attribute still names the live wiki.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Copilot AI review requested due to automatic review settings July 25, 2026 16:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Comment thread citadel/config.py Outdated
Comment thread tests/test_ingest_parallel.py Outdated
Copilot round 5, two comments; the first was factually wrong and the second
right, but both pointed at wording worth sharpening in a concurrency PR.

Measured rather than argued (CPython 3.12): a ThreadPoolExecutor worker does
NOT inherit the submitting thread's context — it reads the ContextVar's default
— and neither does a plain threading.Thread. Context propagation into threads
is an asyncio-task property, not an executor one.

But the same measurement surfaced the caveat neither comment named: a POOLED
thread is reused, and a value one work item leaves set is still there for the
next item on that thread. So wiki_redirect's `finally` reset is load-bearing —
it is what stops a finished source's staging path being handed to the next
source that lands on the same worker. The comment now says both.

The redirect test really did check the main thread while calling it a sibling.
It now checks both shapes while the redirect is held: a genuine sibling worker
(neither thread started the other — the actual --jobs shape) and the main
thread that started the holder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrYKfDw5w6qsFZJR7ssSMt
Copilot AI review requested due to automatic review settings July 25, 2026 16:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

@MarkusNeusinger
MarkusNeusinger merged commit 3b815aa into main Jul 25, 2026
12 checks passed
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.

3 participants