Skip to content

Commit 6ba4bae

Browse files
Merge pull request #139 from MarkusNeusinger/claude/model-ingest-cache-issue-qoc560
ingest: add --reingest, the delete-then-fresh-import escape hatch
2 parents 5a65a77 + 0e16828 commit 6ba4bae

13 files changed

Lines changed: 515 additions & 25 deletions

.github/copilot-instructions.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ session, `--log-dir DIR` writes a transcript per source, `--quiet` drops the liv
3434
`--jobs N`/`-j` folds N sources in CONCURRENTLY (default 1 = serial; `CITADEL_JOBS`),
3535
`--full-rescan` distrusts the manifest's stat cache and re-hashes every tracked source,
3636
`--force <paths>` deliberately re-reads already-ingested sources as a reconcile — it requires
37-
explicit paths and is refused without them, `--retry` re-runs everything STUCK without naming
37+
explicit paths and is refused without them, `--reingest <paths>` goes one step further: each named
38+
tracked source is re-imported FRESH — a `kind="delete"` cleanup session strips its previous facts
39+
(manifest entry dropped), then the same run ingests it as a brand-new source under the current
40+
model + rules — the full re-think reconcile's keep-the-existing-treatment rule deliberately avoids
41+
(requires explicit paths like `--force`; the two flags and `--retry` refuse to combine; a failed
42+
cleanup blocks that source's fresh session), `--retry` re-runs everything STUCK without naming
3843
paths: every failed source still on disk plus every ingested source no wiki page cites — the
3944
zero-entry sources the run report flags as `no_pages` and `status` marks `NO PAGES` — as forced
4045
reconciles; it refuses paths/`--force` and exits 0 when nothing is stuck),
@@ -211,7 +216,13 @@ addressing them through `ingest`. The flow per source:
211216
`CITADEL_MAX_SOURCE_CHARS` is folded in over several passes (all against one staging copy — see
212217
the promote bullet below). `ingest --force <paths>` bypasses the sha short-circuit: the named
213218
sources land in pending as reconciles (a repo re-digests in full), and the manifest is re-stamped
214-
with the current model + rules version.
219+
with the current model + rules version. `ingest --reingest <paths>` rides force's partitioning
220+
but re-imports fresh instead of reconciling: a delete-cleanup job per named tracked source runs
221+
in the deletions group (always first), dropping the manifest key so the pending session plans the
222+
plain ingest/image/audio/pdf kind of a new key (`repo` for a tracked repo — safe only because the
223+
cleanup already stripped the pages a first-time brief would duplicate); a failed cleanup refuses
224+
that source's fresh session, and the cleanup never prunes the transcript/pdftext caches (the
225+
bytes are unchanged and about to be re-read).
215226
- **Discovery is incremental and deletion-safe**: one iterative `os.scandir`
216227
walk over every `CITADEL_RAW_DIRS` root keeps each file's stat; the **manifest doubles as the
217228
scan cache** (an entry's `size`/`mtime_ns`/`ctime_ns`/`hashed_at_ns` are a skip-hint — sha256

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format is based on
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- **`citadel ingest --reingest <paths>` — re-import an already-ingested source from scratch.**
12+
`--force` runs a reconcile, which deliberately keeps the source's existing treatment (genre,
13+
page structure) and only verifies/updates its facts — so a source first ingested under an old
14+
rulebook or a weak model never gets re-thought, and "all ok, nothing new" is the correct
15+
reconcile verdict even when a fresh read would organize it completely differently. `--reingest`
16+
is the escape hatch: for each named tracked source it first runs a `kind="delete"` cleanup
17+
session that strips the source's previous facts from the wiki (dropping its manifest entry),
18+
then — in the same run, deletions always run first — ingests it as a brand-new source under the
19+
current model, rules, and wiki state (fresh cross-links included). This replaces the manual
20+
move-the-file-out / full ingest / move-it-back / ingest dance. A failed cleanup refuses the
21+
fresh session for that source (nothing is written on top of the old facts), tracked repos take
22+
the first-time `repo` brief over a full digest, and the flag requires explicit paths like
23+
`--force` (exit 2 without them; `--force` and `--retry` refuse to combine with it). The
24+
`tasks/delete.md` brief now covers the still-on-disk cleanup case.
25+
26+
### Changed
27+
28+
- **Forced/refresh reconciles now re-read with fresh eyes, not just re-verify.** The
29+
`tasks/reconcile.md` brief (driving `ingest --force` and every `citadel refresh` session) now
30+
explicitly instructs the agent that a re-read is more than verification: mine the unchanged
31+
source for wiki-worthy facts the first ingest missed, cross-link against today's wiki (pages
32+
that did not exist back then may now be the right home or neighbor), and strip claims the
33+
source never actually supported — while still keeping the existing genre treatment (structure
34+
churn stays `--reingest`'s job).
35+
936
## [0.6.0] - 2026-07-30
1037

1138
### Changed (breaking)

CLAUDE.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ session, `--log-dir DIR` writes a transcript per source, `--quiet` drops the liv
3131
`--jobs N`/`-j` folds N sources in CONCURRENTLY (default 1 = serial; `CITADEL_JOBS`),
3232
`--full-rescan` distrusts the manifest's stat cache and re-hashes every tracked source,
3333
`--force <paths>` deliberately re-reads already-ingested sources as a reconcile — it requires
34-
explicit paths and is refused without them, `--retry` re-runs everything STUCK without naming
34+
explicit paths and is refused without them, `--reingest <paths>` goes one step further: each named
35+
tracked source is re-imported FRESH — a `kind="delete"` cleanup session strips its previous facts
36+
(manifest entry dropped), then the same run ingests it as a brand-new source under the current
37+
model + rules — the full re-think reconcile's keep-the-existing-treatment rule deliberately avoids
38+
(requires explicit paths like `--force`; the two flags and `--retry` refuse to combine; a failed
39+
cleanup blocks that source's fresh session), `--retry` re-runs everything STUCK without naming
3540
paths: every failed source still on disk plus every ingested source no wiki page cites — the
3641
zero-entry sources the run report flags as `no_pages` and `status` marks `NO PAGES` — as forced
3742
reconciles; it refuses paths/`--force` and exits 0 when nothing is stuck),
@@ -208,7 +213,13 @@ addressing them through `ingest`. The flow per source:
208213
`CITADEL_MAX_SOURCE_CHARS` is folded in over several passes (all against one staging copy — see
209214
the promote bullet below). `ingest --force <paths>` bypasses the sha short-circuit: the named
210215
sources land in pending as reconciles (a repo re-digests in full), and the manifest is re-stamped
211-
with the current model + rules version.
216+
with the current model + rules version. `ingest --reingest <paths>` rides force's partitioning
217+
but re-imports fresh instead of reconciling: a delete-cleanup job per named tracked source runs
218+
in the deletions group (always first), dropping the manifest key so the pending session plans the
219+
plain ingest/image/audio/pdf kind of a new key (`repo` for a tracked repo — safe only because the
220+
cleanup already stripped the pages a first-time brief would duplicate); a failed cleanup refuses
221+
that source's fresh session, and the cleanup never prunes the transcript/pdftext caches (the
222+
bytes are unchanged and about to be re-read).
212223
- **Discovery is incremental and deletion-safe**: one iterative `os.scandir`
213224
walk over every `CITADEL_RAW_DIRS` root keeps each file's stat; the **manifest doubles as the
214225
scan cache** (an entry's `size`/`mtime_ns`/`ctime_ns`/`hashed_at_ns` are a skip-hint — sha256

citadel/cli.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ def build_parser() -> argparse.ArgumentParser:
116116
"without them, so a whole-corpus re-read (one agent session per source) can never "
117117
"happen by accident.",
118118
)
119+
p_ingest.add_argument(
120+
"--reingest",
121+
action="store_true",
122+
help="Re-import the given already-ingested sources FRESH: first a delete cleanup session "
123+
"strips each source's previous facts from the wiki (its manifest entry is dropped), then "
124+
"the same run ingests it as a brand-new source under the current model + rules — the "
125+
"full re-think --force's reconcile deliberately avoids (reconcile keeps the existing "
126+
"treatment). Costs two sessions per source. Requires explicit paths, like --force.",
127+
)
119128
p_ingest.add_argument(
120129
"--retry",
121130
action="store_true",
@@ -422,11 +431,17 @@ def cmd_ingest(args: argparse.Namespace) -> int:
422431
agent session per source, so forcing the ENTIRE corpus must never happen by accident — the
423432
flag alone is refused with exit 2, before ``ingest.ingest`` is ever called.
424433
434+
``--reingest`` is force's bigger sibling — strip the named sources' previous facts (a delete
435+
cleanup session each), then re-import them fresh in the same run. Same explicit-paths rule as
436+
``--force`` (exit 2 without them), and the two flags refuse each other: reconcile-in-place
437+
and strip-and-re-import are different intents, and combining them would blur which one wins.
438+
425439
``--retry`` is the no-typing complement: it computes its own bounded set —
426440
``ingest.retry_candidates()``, the failed sources still on disk plus the ingested ones no
427441
wiki page cites — prints it, and runs those as a FORCED re-read. It therefore refuses
428-
explicit paths and ``--force`` (exit 2): the point of the flag is that citadel picks the
429-
set, and combining the two would blur which one wins. Nothing to retry is a clean exit 0.
442+
explicit paths, ``--force`` and ``--reingest`` (exit 2): the point of the flag is that
443+
citadel picks the set, and combining them would blur which one wins. Nothing to retry is a
444+
clean exit 0.
430445
431446
``--jobs N`` is a usage error below 1 (exit 2, like ``--force`` without paths) rather than an
432447
exception out of the API layer; omitted, the run takes ``CITADEL_JOBS`` (default 1, serial)."""
@@ -436,11 +451,21 @@ def cmd_ingest(args: argparse.Namespace) -> int:
436451
print(f"error: --jobs must be at least 1 (got {args.jobs}); 1 means the serial default.", file=sys.stderr)
437452
return 2
438453

439-
if args.retry and (args.paths or args.force):
454+
if args.retry and (args.paths or args.force or args.reingest):
440455
print(
441456
"error: --retry computes its own retry set (every failed source plus every ingested "
442-
"source no wiki page cites) — combine no paths and no --force with it; use "
443-
"`citadel ingest --force <paths>` for a hand-picked re-read instead.",
457+
"source no wiki page cites) and cannot be combined with explicit paths, --force, or "
458+
"--reingest; use `citadel ingest --force <paths>` (or `--reingest <paths>`) for a "
459+
"hand-picked re-read instead.",
460+
file=sys.stderr,
461+
)
462+
return 2
463+
464+
if args.force and args.reingest:
465+
print(
466+
"error: --force and --reingest are different intents — --force reconciles the "
467+
"source's facts in place (keeping its existing treatment), --reingest strips them "
468+
"and re-imports the source fresh. Pick one.",
444469
file=sys.stderr,
445470
)
446471
return 2
@@ -453,6 +478,15 @@ def cmd_ingest(args: argparse.Namespace) -> int:
453478
)
454479
return 2
455480

481+
if args.reingest and not args.paths:
482+
print(
483+
"error: --reingest requires explicit paths (each source runs a delete cleanup "
484+
"session plus a fresh ingest session; name the files or directories to re-import, "
485+
"e.g. `citadel ingest --reingest raw/notes.md`).",
486+
file=sys.stderr,
487+
)
488+
return 2
489+
456490
paths = args.paths or None
457491
force = args.force
458492
if args.retry:
@@ -486,7 +520,9 @@ def cmd_ingest(args: argparse.Namespace) -> int:
486520
# CITADEL_LLM_VERBOSE — not just the --verbose flag — also drops the spinner that would
487521
# otherwise clobber the streamed transcript.
488522
progress = ConsoleProgress(spinner=not config.LLM_VERBOSE)
489-
report = ingest.ingest(paths, progress=progress, full_rescan=args.full_rescan, force=force, jobs=args.jobs)
523+
report = ingest.ingest(
524+
paths, progress=progress, full_rescan=args.full_rescan, force=force, jobs=args.jobs, reingest=args.reingest
525+
)
490526
print(report.render())
491527
# Non-zero on a per-source error OR a structural problem left behind (a broken
492528
# cross-link the agent introduced) — so ingest gates the wiki's integrity in CI.

0 commit comments

Comments
 (0)