Version: @kaelio/ktx@0.16.0 (npm latest); main unchanged. Backend: claude-code, descriptions=haiku.
Summary
Re-running ktx ingest re-sends every table/column description to the LLM even when the schema is completely unchanged. Cross-run resume never hits, so on a large DB every run effectively "restarts from 0" and exhausts the rate-limit window. The cause is that a per-scan wall-clock timestamp is folded into the description resume hash.
Root cause
computeKtxDescriptionsStageHash hashes the whole snapshot, and the snapshot carries extractedAt, set fresh on every scan:
// packages/cli/src/context/scan/enrichment-state.ts
export function computeKtxDescriptionsStageHash(input) {
return stableContentHash({ snapshot: input.snapshot, llmIdentity: input.llmIdentity });
}
Connectors set extractedAt: this.now().toISOString() on every introspection (e.g. connectors/postgres/connector.ts). Both persistent caches key on this hash, so a fresh extractedAt = new hash = full miss:
- SQLite stage cache —
runEnrichmentStage → stateStore.findCompletedStage({ connectionId, stage, inputHash }). On a hit it skips the LLM entirely.
- File resume store —
enrichment-progress/descriptions.json (local-enrichment-artifacts.ts).
Evidence it's the timestamp, not data drift
Two scans of the identical table set: every per-table staged snapshot and the FK graph were byte-identical; the only diff was connection.json's timestamp:
extractedAt: "2026-07-09T06:49:07.775Z" → "2026-07-09T07:11:45.930Z"
…and the descriptions hash moved 55abf581… → cac78360…, forcing full regeneration. The SQLite cache (local_content_results, namespace scan:descriptions) shows the two scans as two rows that never dedupe.
Reproduction (any warehouse)
ktx ingest <conn> — note the descriptions and enrichment-progress/descriptions.json inputHash.
ktx ingest <conn> again (fresh scan, unchanged schema). Observe: descriptions reworded, new inputHash, and the SQLite scan:descriptions cache gains a second row instead of hitting the first.
Expected: an unchanged schema reuses prior descriptions and makes no LLM calls.
Impact
Every re-scan re-sends all tables to the LLM. On a Pro/Max subscription this exhausts the window; with ingest.workUnits.failureMode: continue, rate-limited tables are then left with empty descriptions.ai rather than pausing. No ktx.yaml option or CLI flag avoids it (--stages descriptions still recomputes the hash).
Fix
Exclude extractedAt (and any per-scan metadata) from computeKtxDescriptionsStageHash — hash only schema-identifying content. This mirrors code you already have: stableLiveDatabaseHashContent in local-stage-ingest.ts already does delete stable.extractedAt before hashing connection.json for the ingest work-unit cache; the scan-enrichment stage hash just needs the same treatment (applied to all three stage hashes, since embeddings/relationships hash the snapshot too).
Verified locally (built from source): with the strip, a second ktx ingest on an unchanged schema makes zero description LLM calls, keeps a stable stage hash, and writes no new write/flush enrichment descriptions commits. A unit regression (two snapshots differing only in extractedAt must hash equal) fails on main and passes with the fix; a real schema change still re-keys.
Scope note
This fixes re-runs against an unchanged schema (the common case: interruptions, retries, refreshes). Adding/removing/changing a table still re-keys the whole batch and regenerates unchanged tables too — that's a separate, deeper gap (per-table checkpointing) tracked as a feature request.
Version:
@kaelio/ktx@0.16.0(npm latest);mainunchanged. Backend:claude-code, descriptions=haiku.Summary
Re-running
ktx ingestre-sends every table/column description to the LLM even when the schema is completely unchanged. Cross-run resume never hits, so on a large DB every run effectively "restarts from 0" and exhausts the rate-limit window. The cause is that a per-scan wall-clock timestamp is folded into the description resume hash.Root cause
computeKtxDescriptionsStageHashhashes the whole snapshot, and the snapshot carriesextractedAt, set fresh on every scan:Connectors set
extractedAt: this.now().toISOString()on every introspection (e.g.connectors/postgres/connector.ts). Both persistent caches key on this hash, so a freshextractedAt= new hash = full miss:runEnrichmentStage→stateStore.findCompletedStage({ connectionId, stage, inputHash }). On a hit it skips the LLM entirely.enrichment-progress/descriptions.json(local-enrichment-artifacts.ts).Evidence it's the timestamp, not data drift
Two scans of the identical table set: every per-table staged snapshot and the FK graph were byte-identical; the only diff was
connection.json's timestamp:…and the descriptions hash moved
55abf581… → cac78360…, forcing full regeneration. The SQLite cache (local_content_results, namespacescan:descriptions) shows the two scans as two rows that never dedupe.Reproduction (any warehouse)
ktx ingest <conn>— note the descriptions andenrichment-progress/descriptions.jsoninputHash.ktx ingest <conn>again (fresh scan, unchanged schema). Observe: descriptions reworded, newinputHash, and the SQLitescan:descriptionscache gains a second row instead of hitting the first.Expected: an unchanged schema reuses prior descriptions and makes no LLM calls.
Impact
Every re-scan re-sends all tables to the LLM. On a Pro/Max subscription this exhausts the window; with
ingest.workUnits.failureMode: continue, rate-limited tables are then left with emptydescriptions.airather than pausing. Noktx.yamloption or CLI flag avoids it (--stages descriptionsstill recomputes the hash).Fix
Exclude
extractedAt(and any per-scan metadata) fromcomputeKtxDescriptionsStageHash— hash only schema-identifying content. This mirrors code you already have:stableLiveDatabaseHashContentinlocal-stage-ingest.tsalready doesdelete stable.extractedAtbefore hashingconnection.jsonfor the ingest work-unit cache; the scan-enrichment stage hash just needs the same treatment (applied to all three stage hashes, since embeddings/relationships hash the snapshot too).Verified locally (built from source): with the strip, a second
ktx ingeston an unchanged schema makes zero description LLM calls, keeps a stable stage hash, and writes no newwrite/flush enrichment descriptionscommits. A unit regression (two snapshots differing only inextractedAtmust hash equal) fails onmainand passes with the fix; a real schema change still re-keys.Scope note
This fixes re-runs against an unchanged schema (the common case: interruptions, retries, refreshes). Adding/removing/changing a table still re-keys the whole batch and regenerates unchanged tables too — that's a separate, deeper gap (per-table checkpointing) tracked as a feature request.