Skip to content

feat(kb): wire dead-letter document reconciler Lambda + nightly schedule + IAM (task 16.5) - #1008

Open
DerrickF wants to merge 2 commits into
developfrom
feat/kb-deadletter-reconcile-infra
Open

feat(kb): wire dead-letter document reconciler Lambda + nightly schedule + IAM (task 16.5)#1008
DerrickF wants to merge 2 commits into
developfrom
feat/kb-deadletter-reconcile-infra

Conversation

@DerrickF

@DerrickF DerrickF commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What

Wires the dead-letter document reconciler (built in #1007) into infrastructure: a fifth kb-migration Lambda, a nightly schedule, and its IAM. Until this lands, the reconciler module exists but nothing runs it.

Stacked on #1007. Its commit shows here until #1007 merges to develop; merge this after #1007. No infrastructure is deployed by this PR.

Changes

  • Fifth Lambda, same one image. document_reconciler.lambda_handler joins the existing one-image / (now) five-function construct via an ImageConfig.Command override — one build, one ECR push, one out-of-band update-function-code per deploy. Bootstrap stub + Dockerfile COPY keep the byte-stable image consistent.
  • Nightly schedule. cron(0 9 * * ? *) — 09:00 UTC, ≈ 02:00–03:00 America/Denver — rather than rate(1 day), so "nightly" means night, not "24h after each deploy". ENABLED regardless of flags, because it ships report-only (MANAGED_KB_DOC_RECONCILER_ARMED off) and report-only is read-only — the same inverted convention as the KB reconciler, so the audit period actually happens.
  • IAM, deliberately narrow. grantDirectIngestion (GetKnowledgeBaseDocuments + IngestKnowledgeBaseDocuments) + grantRetrieval (bedrock:Retrieve) + documents-bucket read + assistants-table read/write. Not grantProvisioning and not iam:PassRole: it reads KB_Records from DynamoDB (never ListKnowledgeBases) and never creates or deletes a knowledge base — a strictly narrower footprint than the KB reconciler beside it.
  • New flag. MANAGED_KB_DOC_RECONCILER_ARMED (config.docReconcilerArmed), empty-string = off, forwarded to every function and threaded through load-env.sh → context → config.ts. Ships disarmed.
  • Deploy plumbing. SSM function-name param, a deploy-image-lambda-one.sh case, and a backend.yml deploy step so the image swap reaches the new function.

Why nightly

A dead-lettered document is a rare event, and the reconciler is a recovery sweep, not a hot path. Nightly matches the existing KB reconciler's rhythm; the only cost, once armed, is that a stranded-but-retrievable doc could stay invisible up to ~a day — acceptable for recovery. Tighten to hourly in one line here if dead-letters ever prove common.

Tests

Full infra jest suite green (792). Updated every "exactly four" assertion to five (Lambda count, schedules, SSM slugs, flag forwarding), added the nightly-schedule + doc-reconciler-footprint assertions, and updated the retrieval/direct-ingestion holder counts in managed-kb.test.ts. Touched Python supply-chain tests green (21): the import-closure walk gains the new entrypoint, and the env-contract test moves the flag from an exemption to a spot-pinned load-bearing variable now that the construct sets it. tsc --noEmit clean.

Not in this PR

Turning the reconciler on. That is a separate, reviewed step: flip MANAGED_KB_DOC_RECONCILER_ARMED after the report-only audit period, and only after a deploy is agreed.

The ingestion consumer is the only writer of DOC# status. When its event
dead-letters (Lambda async retry capped at 2), a document Bedrock already
indexed is left parked non-terminal forever, and the retrieval filter serves
only 'complete' -- so its content sits in the KB fully retrievable and invisible
to every query. Two such docs occurred in dev; both needed manual repair.

Add document_reconciler.py: the missing second writer. Daily, it finds DOC# rows
stuck non-terminal (uploading/chunking/embedding) past a 60-minute grace gate,
probes Bedrock per document, and drives a stranded-but-retrievable doc to
'complete' (the §5.37 case). It reuses the consumer's own probes -- document_status,
the equals-on-document_id retrievability search, its status-set constants, and
set_document_terminal -- so §5.37/§5.38/§5.39 live in one place. FAILED -> failed;
NOT_FOUND -> re-ingest from S3 (the scheduled form of task 14.4's one-click retry).

Modelled on reconciler.py: ships DISARMED (MANAGED_KB_DOC_RECONCILER_ARMED, empty
reads as off); per-run action limit applies in both modes so the report is
trustworthy; grace gate is a pure function of the row's own updatedAt and fails
closed; terminal/deleting rows are never candidates.

Guards in tests/lambdas/test_kb_document_reconciler.py (61 tests), mutation-verified.
Scheduling + IAM wiring + arming are a deploy-gated follow-up; the flag is exempted
in the env-contract test's OPTIONAL_OVERRIDES until that lands.
…hedule and IAM (task 16.5)

Adds the fifth kb-migration Lambda (document_reconciler.lambda_handler) to the
shared one-image/five-functions construct, so the reconciler built in the
backend PR actually runs.

- Nightly EventBridge schedule: cron(0 9 * * ? *) (~02:00-03:00 America/Denver),
  ENABLED regardless of flags, because it ships report-only and report-only is
  read-only (same inverted convention as the KB reconciler).
- IAM: grantDirectIngestion (GetKnowledgeBaseDocuments + IngestKnowledgeBaseDocuments)
  + grantRetrieval (bedrock:Retrieve) + documents-bucket read + assistants-table RW.
  Deliberately NOT grantProvisioning or PassRole: it reads KB_Records from DynamoDB,
  never ListKnowledgeBases, and never creates/deletes a knowledge base -- a strictly
  narrower footprint than the KB reconciler.
- New flag MANAGED_KB_DOC_RECONCILER_ARMED (config.docReconcilerArmed), empty=off,
  forwarded to every function and threaded through load-env.sh; ships disarmed.
- SSM function-name param + deploy-image-lambda-one.sh case + backend.yml deploy
  step so the out-of-band image swap reaches the new function.
- Bootstrap stub (document_reconciler.py) + Dockerfile COPY keep the byte-stable
  five-handler image consistent; import-closure and env-contract tests updated.

Full infra suite green (792), touched Python supply-chain tests green (21).
Stacked on the backend PR (#1007); merge after it. No deploy in this PR.
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