PLAN — On-prem deployment pivot: B1 co-locate cocoindex + pullmd on one host (ID-66.4)
PLAN — On-prem deployment pivot: B1 co-locate cocoindex + pullmd on one host (ID-66.4)
Section titled “PLAN — On-prem deployment pivot: B1 co-locate cocoindex + pullmd on one host (ID-66.4)”Task: ID-66 — On-prem deployment pivot — B1 co-locate cocoindex + pullmd on one host.
Subtask: {66.4} PLAN (decomposition into {66.5+} implementation Subtasks).
Author date: 29/05/2026.
Predecessors (ratified): RESEARCH.md ({66.1}), PRODUCT.md ({66.2}, 25 numbered
behaviour invariants), TECH.md ({66.3}, 9 Proposed changes + 10-step migration
ordering + invariant coverage map), OQ-CLOSURE.md (OQ-66-1/2/3/4/6 closed). Authored by a
fresh Planner instance per Q-PLANNER-2 — PRODUCT.md and TECH.md were read in full as
input, not assumed.
Filename convention (ID-48.4): canonical
{N.4}artefact isPLAN.md. This is the decomposition only — the Subtask records below are drafted inside this document for the parent to transcribe intodocs/reference/task-list.jsonverbatim. PLAN does not edittask-list.json, the roadmap, the backlog, or any code.
Overview
Section titled “Overview”B1 re-platforms the ingestion pipeline only (cocoindex + pullmd + Playwright/Trafilatura
sidecars) off Google Cloud Run onto one IONOS VPS managed by Coolify. The product
app (Vercel + managed Supabase) is untouched — there is no app cutover and no KH
application code change anywhere. The whole decomposition is infrastructure/ops work:
Compose YAML, a thin GitHub Action, persistent volumes, secret migration, a backup cron, a
datapath monitor, and host/DNS config. The only KH-corpus seam is the single inbound
pipeline-runs/record webhook, which is repointed by env var only (no source change,
OQ-66-2).
This PLAN maps the TECH 10-step migration ordering into 14 implementation Subtasks
({66.5}–{66.18}), comfortably under the 25-Subtask soft ceiling. The heaviest area
(TECH change 4 — the thin GH Action + ghcr.io + Coolify deploy webhook + workflow
retirement) is split into four discrete slices ({66.7} build-and-push, {66.11} Coolify
webhook wiring, {66.12} rollback-by-tag, {66.17} workflow retirement). The kpf
dead-manifest cleanup (change 9) is a clearly-labelled standalone side-task ({66.18}).
Code-intelligence orientation (cited verbatim per the planner-block)
Section titled “Code-intelligence orientation (cited verbatim per the planner-block)”This is an infrastructure/ops artefact. The orientation tool was run against the KH
corpus per .gitnexus/CLAUDE.md; the gitnexus index is STALE (last indexed b8d9f89),
so file paths are authoritative and exact line numbers are treated as approximate.
gitnexus_query({query: "cocoindex pipeline deploy cloudrun", repo: "knowledge-hub"})→"processes": [](no execution flows indexed for this deploy surface — the pipeline has never run end-to-end, consistent with PRODUCT §Problem). It returned onlydefinitions[]: pipeline + server symbolsstart_cocoindex_thread(scripts/cocoindex_pipeline/server.py:168-225),_target(scripts/cocoindex_pipeline/server.py:203-217),_build_db_ctx(scripts/cocoindex_pipeline/flow.py:651-673),_build_dsn(scripts/cocoindex_pipeline/flow.py:969-1005),app_main(scripts/cocoindex_pipeline/flow.py:1523-1867), plus a cluster of pipeline test modules underscripts/tests/. No execution process binds these — confirming the deploy/co-location surface is greenfield infra plumbing, not a re-wire of an indexed call graph.- External-library grounding (OQ-3 / Q-EX2): this artefact asserts no importable
third-party library symbol shape as load-bearing, so no import-and-call check is
required. The cocoindex→pullmd repoint is env-only (
PULLMD_SERVICE_URL,scripts/cocoindex_pipeline/adapters.py:125, OQ-66-2).cocoindex[postgres]==1.0.3is already empirically pinned (requirements.txt); no cocoindex symbol shape is asserted.mdb_copyis an LMDB CLI tool, not an importable symbol (MVCC hot-copy safety grounded in OQ-CLOSURE OQ-66-4). Coolify is a hosted-platform capability set, not an importable API (Compose build pack, per-app encrypted env, S3 DB backup, deploy webhook, tag-edit rollback, auto-Let’s-Encrypt — grounded in RESEARCH §2 + OQ-CLOSURE OQ-66-3/4/6).
Code-touching discipline note (for the Executor)
Section titled “Code-touching discipline note (for the Executor)”These Subtasks are largely NON-code-touching in the KH sense — they create/edit
Compose YAML, host config, Coolify env, a backup script, a monitor script, and
documentation. The gitnexus impact-analysis discipline (gitnexus_impact before edit) is
therefore mostly N/A because no KH Python/TS symbol is modified.
The one exception: any edit to .github/workflows/*.yml on the deploy path
({66.7}, {66.11}, {66.17}, {66.18}) — the Executor should still run
gitnexus_detect_changes() before committing those, to confirm the change scope is the
deploy workflow and nothing else.
The standing guard repeated in every Subtask details: NO KH application code
changes — scripts/cocoindex_pipeline/adapters.py, scripts/cocoindex_pipeline/flow.py,
and the Vercel route app/api/internal/pipeline-runs/record/route.ts are unchanged; every
repoint is env-only (OQ-66-2). Do NOT “fix” the env-only repoint in source.
Cross-Task hand-off deliverable to ID-62 (UNMISSABLE — read this section)
Section titled “Cross-Task hand-off deliverable to ID-62 (UNMISSABLE — read this section)”ID-62 (fixture-staging-infra) has Task.dependencies including ID-66. This dependency
is a Task-level dependency (correct per §3.3 — cross-Task deps live at the Task level,
NOT the Subtask level). ID-62 cannot begin its live-verification implementation until ID-66
delivers a reachable, datapath-proven host.
The Subtask that carries this hand-off deliverable is {66.16} — “First end-to-end
ingest smoke + datapath proof + ID-62 reachability hand-off”. Its acceptance is the gate
ID-62 depends on:
- The co-located host is running — all four containers up on one Coolify-managed compose network (invariant 1).
- The datapath is proven end-to-end — a real ingestion runs and a genuine
pipeline_runsrow lands in Supabase via the repointed webhook (invariants 8, 9, 21), proving the {62.1} reachability defect is dissolved (invariant 3) — the cocoindex→pullmd hop that could not route on Cloud Run no longer exists because the two share a host-local network. - A host-facing HTTPS ingress surface is reachable — Coolify auto-Let’s-Encrypt
serves the host over HTTPS, and an external client (e.g. a GitHub-hosted runner) can
reach a host-facing endpoint over HTTPS — i.e. the
ingress:internal/403 problem that blocked ID-62 from CI (its defect (2)) is gone because the host is publicly reachable.
The /stage boundary (OQ-66-8 — assumption documented, pending confirmation). Per the
analysis appended to OQ-pending.md as ## OQ-66-8, ID-62’s /stage is a fixture-drop
HTTP server that ID-62 itself builds (its {62.2}/{62.3} are PRODUCT/TECH for the
/stage co-resident route; ID-62 found NO /stage handler exists yet — ID-49.10 built
only the client helper). ID-66’s hand-off is therefore the reachable HTTPS host + the
proven datapath + the provisioned ingress surface onto which ID-62 will later mount its own
/stage route — ID-66 does NOT implement /stage (that would be KH/pipeline code,
out of B1 scope per OQ-66-2).
Assumption adopted (OQ-66-8): “host reachable over HTTPS + datapath proven + ingress surface provisioned” is the ID-66→ID-62 hand-off. If Liam/parent confirms ID-66 must instead pre-provision something more specific (a reserved hostname/subdomain, a Coolify app slot, or a published
COCOINDEX_FIXTURE_STAGING_URLbase value),{66.16}’s acceptance widens accordingly. PLAN proceeds on the documented assumption; this is flagged in{66.16}’sdetails.
Subtask records (TM-shape — for the parent to transcribe into task-list.json)
Section titled “Subtask records (TM-shape — for the parent to transcribe into task-list.json)”All
idvalues are local to parent Task 66 (the ledger renders them as{66.5}…{66.18}).dependenciesare sibling-only integer arrays referencing other Subtasks of THIS Task.description≤250 chars HARD,testStrategy≤300 chars HARD (parent’s CLI budget gate REJECTS over-budget records).statusinitialises topending.
{66.5} — Stand up IONOS VPS + base hardening
Section titled “{66.5} — Stand up IONOS VPS + base hardening”- id: 5
- title: Stand up IONOS VPS + base hardening
- description: Provision the IONOS VPS (8 vCPU / 16 GB / 480 GB NVMe, ~£21/mo per OQ-66-1) and apply base hardening — SSH key-only auth, firewall, OS updates. The host for the co-located pipeline.
- details: TECH migration step 1; satisfies the host substrate for invariant 1 (single-host co-location). Provision the IONOS VPS per OQ-66-1 (8 vCPU / 16 GB / 480 GB NVMe). Harden: SSH key-only (disable password auth), firewall (allow only SSH + the HTTPS/HTTP ports Coolify needs), apply OS security updates. Record host IP + access in the new B1 runbook (proposed
docs/runbooks/onprem-b1-deploy.md). NO KH application code changes — this is host provisioning only;adapters.py/flow.py/the Vercel route are untouched. No code-intel impact discipline (no KH symbol touched). - testStrategy: Operator SSHes into the IONOS VPS with key-only auth (password auth refused);
nproc/free -g/df -hconfirm 8 vCPU / 16 GB / 480 GB NVMe; firewall denies all but the intended ports; OS is patched. - dependencies: []
- priority: high
{66.6} — Install Coolify + verify compose build pack
Section titled “{66.6} — Install Coolify + verify compose build pack”- id: 6
- title: Install Coolify + verify compose build pack
- description: Run the Coolify one-script install on the VPS; verify the dashboard loads and the Docker Compose build pack is available. Coolify is the PaaS layer replacing the Cloud Run scaffolding.
- details: TECH migration step 2. Run the Coolify one-script install on the
{66.5}host. Verify: the Coolify dashboard is reachable; the Docker Compose build pack is selectable (RESEARCH §2.3 — the direct fit for the existing pullmd compose unit); Coolify’s per-app encrypted env, S3-backup, deploy-webhook, and auto-Let’s-Encrypt capabilities are present (grounded OQ-66-3/4/6). Record dashboard URL + admin access in the B1 runbook. NO KH application code changes. No code-intel impact discipline (no KH symbol touched). - testStrategy: Coolify dashboard loads over HTTPS; an operator can create a new application using the Docker Compose build pack; per-app env, S3-backup, and deploy-webhook features are visible in the UI.
- dependencies: [5]
- priority: high
{66.7} — Thin GH Action: build cocoindex image off-host → push pinned to ghcr.io
Section titled “{66.7} — Thin GH Action: build cocoindex image off-host → push pinned to ghcr.io”- id: 7
- title: GH Action — build off-host, push pinned image to ghcr.io
- description: Author a thin GitHub Action that builds the ~9 GB cocoindex image on GH-hosted runners and pushes a pinned (sha/semver, never
:latest) image to ghcr.io. The heavy build never runs on the 16 GB host. - details: TECH change 4 slice A; TECH migration step 6 (part). Satisfies invariants 13, 14 (push half). Create
.github/workflows/onprem-deploy.yml. On push to the deploy branch (pathsscripts/**/*.py,requirements.txt,deploy/onprem/**), build the cocoindex image on GitHub-hosted runners (the ~9 GB two-image build — cocoindex ~5.3 GB + Playwright ~3.7 GB — MUST NOT run on the 16 GB host; OQ-66-6). Push to ghcr.io under a pinned content-addressable tag (:sha-<gitsha>or:<semver>, never:latest— mirrors the existing pullmd no-:latestdiscipline). This Subtask covers build-and-push ONLY; the Coolify pull/deploy webhook is{66.11}..ymledit → Executor MUST rungitnexus_detect_changes()before commit to confirm scope. NO KH application code changes — the build packages existingscripts/cocoindex_pipeline/*.pyunchanged; do not edit source.NEXT_PUBLIC_CLIENT_IDis NOT a B1-host concern (OQ-66-6 — Vercel-side bake). - testStrategy: A push to the deploy branch triggers the Action; the build runs on a GH-hosted runner (not the host); a pinned, non-
:latestimage tag appears in ghcr.io and is pullable; the host CPU/RAM is uncontended during the build. - dependencies: []
- priority: high
{66.8} — Author + deploy the co-location docker-compose
Section titled “{66.8} — Author + deploy the co-location docker-compose”- id: 8
- title: Author + deploy the co-location docker-compose
- description: Author
deploy/onprem/docker-compose.ymldeclaring cocoindex + pullmd + Playwright + Trafilatura on one Coolify compose network; setPULLMD_SERVICE_URL=http://pullmd:3000(env-only repoint). Deploy via Coolify. - details: TECH change 1; TECH migration step 3. Satisfies invariants 1, 2, 3, 4, 11. Create
deploy/onprem/docker-compose.ymlwith four services on one Coolify-managed compose network: cocoindex (ghcr.io/<org>/kh-cocoindex-pipeline:<pinned-tag>from{66.7}), envPULLMD_SERVICE_URL=http://pullmd:3000(compose alias — repoint is env-only, no source change per OQ-66-2), retainPULLMD_API_TOKENBearer as defence-in-depth, carry overCOCOINDEX_SOURCE_PATH/COCOINDEX_DB/COCOINDEX_DB_DSNboot-required discipline; pullmd (aeternalabshq/pullmd:2.0.0pinned, port 3000,PULLMD_AUTH_MODE=single-admin,PLAYWRIGHT_URL=http://playwright:8002/render,TRAFILATURA_URL=http://trafilatura:8001/extract); playwright (aeternalabshq/pullmd-playwright:2— resolve:2→digest before deploy); trafilatura (aeternalabshq/pullmd-trafilatura:2— same digest-pin). The LMDB/corpus volumes are mounted by{66.9}; secrets are injected by{66.10}. A/healthhealthcheck MAY be kept for container-restart only — it is explicitly NOT the datapath signal ({66.15})..yml/compose only — NO KH application code changes; do not editadapters.py. - testStrategy: Operator sees all four containers on one compose network; a Bearer-authed
GET http://pullmd:3000/api?url=…issued from inside the cocoindex container returns a pullmd response over the alias (never a*.run.appURL); noingress:internal/VPC/NAT in the path. - dependencies: [6, 7]
- priority: high
{66.9} — Create + mount the LMDB persistent volume + corpus volume
Section titled “{66.9} — Create + mount the LMDB persistent volume + corpus volume”- id: 9
- title: Create + mount LMDB persistent volume + corpus volume
- description: Replace the Cloud Run in-RAM tmpfs with a disk-backed Docker named volume mounted at the cocoindex
COCOINDEX_DBparent path on local NVMe; add the corpus volume. Set a generousCOCOINDEX_LMDB_MAP_SIZE. - details: TECH change 2; TECH migration step 4. Satisfies invariants 5, 6, 7. In
deploy/onprem/docker-compose.yml(from{66.8}), declare a Docker named volume / Composevolumes:mount on the host’s local NVMe, mounted at the cocoindexCOCOINDEX_DBparent path (/cocoindex-state, so the engine store lands at/cocoindex-state/lmdb). The volume is disk-backed, NOT tmpfs (local NVMemmaps + locks LMDB natively — LMDB’s hard requirement; GCS-FUSE never could). Add the corpus volume forCOCOINDEX_SOURCE_PATH. SetCOCOINDEX_LMDB_MAP_SIZEgenerously (a few GiB — sparse virtual reservation, not disk pre-alloc) to avoidMDB_MAP_FULLre-tune. Preserve the engine-LMDB-vs-asyncpg-DSN distinction (cocoindex-deploy.md §0): this volume backsCOCOINDEX_DB(engine store), NOTCOCOINDEX_DB_DSN(Supabase pool). Document in the B1 runbook that LMDB loss forces a memo-rebuild only, never KH data loss (Supabase canonical, inv 7). NO KH application code changes. - testStrategy: An operator inspecting the mount sees a disk-backed volume, not
tmpfs; writing memo state, restarting the cocoindex container without destroying the volume, then observing the state intact confirms persistence; the runbook documents LMDB loss = memo-rebuild only, never KH data loss. - dependencies: [8]
- priority: high
{66.10} — Migrate secrets into Coolify per-app encrypted env
Section titled “{66.10} — Migrate secrets into Coolify per-app encrypted env”- id: 10
- title: Migrate secrets into Coolify per-app encrypted env
- description: One-time manual copy of the ~8–10 runtime secret values out of GCP Secret Manager into Coolify per-app encrypted env (OQ-66-3). Drop, not migrate, the GCP deploy-SA/WIF creds.
PULLMD_SERVICE_URLbecomes a non-secret. - details: TECH change 3; TECH migration step 5. Satisfies invariants 10, 11, 12. For each value run
gcloud secrets versions access latest --secret=<name>and paste into Coolify’s per-app encrypted env (hidden-after-save; Infisical deferred to B2). Base set (cocoindex):ANTHROPIC_API_KEY,OPENAI_API_KEY,SUPABASE_URL,SUPABASE_PUBLISHABLE_KEY,SUPABASE_SERVICE_ROLE_KEY,NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,NEXT_PUBLIC_APP_URL,CRON_SECRET,SENTRY_AUTH_TOKEN. cocoindex-specific:COCOINDEX_DB_DSN(Supabase pooler, boot-required),PIPELINE_RUN_WEBHOOK_URL(set in{66.13}),PULLMD_API_TOKEN. pullmd:PULLMD_ADMIN_EMAIL,PULLMD_ADMIN_PASSWORD,PULLMD_API_TOKEN.PULLMD_SERVICE_URLis NOT a secret — it is the plainhttp://pullmd:3000(inv 11). DROPPED, not migrated: the GCP deploy-SA / WIF credentials — no deploy-SA JSON ever lands on the host (inv 12). NO KH application code changes. - testStrategy: Auditing Coolify env shows the runtime secret set stored encrypted/hidden, not read from GCP Secret Manager at runtime;
PULLMD_SERVICE_URLis a plain non-secret value; a host secrets audit finds NO GCP deploy-SA JSON — runtime secrets only. - dependencies: [6]
- priority: high
{66.11} — Wire Coolify deploy webhook → pull pinned image
Section titled “{66.11} — Wire Coolify deploy webhook → pull pinned image”- id: 11
- title: Wire Coolify deploy webhook → pull pinned image
- description: Extend
onprem-deploy.ymlto call Coolify’s deploy webhook/API after the ghcr.io push so the host pulls and deploys the pinned image. The host pulls finished images; it never builds. - details: TECH change 4 slice B; TECH migration step 6 (part). Satisfies invariant 14 (pull half). Extend
.github/workflows/onprem-deploy.yml(from{66.7}): after the pinned-image push to ghcr.io, call Coolify’s deploy webhook/API to pull and deploy that pinned tag onto the{66.8}compose app. Store the Coolify webhook URL/token as a GitHub secret (NOT on the host). Confirm Coolify is configured to pull from ghcr.io (registry credentials in Coolify if the package is private). The host pulls finished images; it does not build (OQ-66-6)..ymledit → Executor MUST rungitnexus_detect_changes()before commit. NO KH application code changes. - testStrategy: A push that builds+pushes a pinned image (via
{66.7}) then triggers the Coolify deploy webhook; Coolify pulls the exact pinned tag from ghcr.io and redeploys the compose app; the running cocoindex container reports the new image digest; the host performed no build. - dependencies: [6, 7]
- priority: high
{66.12} — Rollback-by-tag mechanics + runbook
Section titled “{66.12} — Rollback-by-tag mechanics + runbook”- id: 12
- title: Rollback-by-tag mechanics + runbook
- description: Document and verify the rollback path: re-point the deployed image tag to a prior pinned tag via Coolify’s API/UI and redeploy — near-instant pull, no host rebuild. Roll-forward is the same op with a newer tag.
- details: TECH change 4 slice C; TECH migration step 6 (part). Satisfies invariant 15. Document in the B1 runbook (
docs/runbooks/onprem-b1-deploy.md) the rollback = re-point image tag, no rebuild procedure: set the deployed image tag to a prior pinned ghcr.io tag via Coolify’s API/UI and redeploy (near-instant pull; OQ-66-6 confirms Coolify supports tag-edit rollback). Roll-forward is the identical op with a newer tag. Verify by deploying tag A, then re-pointing to a prior tag B and confirming the container runs B without any rebuild. This is documentation + a verified manual procedure — no workflow change (the webhook from{66.11}already supports it). NO KH application code changes. - testStrategy: With two pinned tags in ghcr.io, an operator re-points the Coolify deployed tag from the newer to the prior tag and redeploys; the container runs the prior image within seconds and no build is triggered on the host; the runbook documents the exact steps.
- dependencies: [11]
- priority: medium
{66.13} — Repoint inbound webhook env + DNS/SSL (auto-Let’s-Encrypt)
Section titled “{66.13} — Repoint inbound webhook env + DNS/SSL (auto-Let’s-Encrypt)”- id: 13
- title: Repoint inbound webhook env + DNS/SSL
- description: Set the cocoindex host env
PIPELINE_RUN_WEBHOOK_URLto the same Vercel app URL with the sameCRON_SECRET(env-only, zero app change), and configure DNS + Coolify auto-Let’s-Encrypt HTTPS for any host-facing endpoint. - details: TECH change 7; TECH migration step 7. Satisfies invariants 8, 9. Set the cocoindex container’s Coolify env
PIPELINE_RUN_WEBHOOK_URLto the same Vercel app URL the Cloud Run deploy used, with the sameCRON_SECRET(from{66.10})._emit_pipeline_run_webhook(scripts/cocoindex_pipeline/flow.py:552-645) reads both fromos.environ— NO source change. ZERO changes to app code,proxy.ts, the route handler (app/api/internal/pipeline-runs/record/route.ts), or Vercel config — the endpoint, itsverifyCronAuth, and itsrecordPipelineRun→pipeline_runswrite are byte-for-byte as today (OQ-66-2). Also configure DNS + Coolify auto-Let’s-Encrypt to serve the host over HTTPS for any host-facing endpoint (this provisions the ingress surface ID-62 will later use — see{66.16}+ OQ-66-8). NO KH application code changes — the webhook repoint is two host env vars only; do NOT editflow.py. - testStrategy: The only change is two host env vars (
PIPELINE_RUN_WEBHOOK_URL,CRON_SECRET); the app/route/proxy.ts/Vercel config are untouched; the host serves HTTPS via Coolify auto-Let’s-Encrypt with a valid cert; agit diffshows no change toflow.pyor the route handler. - dependencies: [8, 10]
- priority: high
{66.14} — LMDB mdb_copy → S3 backup job
Section titled “{66.14} — LMDB mdb_copy → S3 backup job”- id: 14
- title: LMDB mdb_copy → S3 backup job
- description: Author a scheduled job (
deploy/onprem/backup/lmdb-backup.sh) that runsmdb_copy -cfor an LMDB-consistent hot snapshot, then tars/ships it to the same S3 bucket Coolify uses. Document the restore path. - details: TECH change 5; TECH migration step 8. Satisfies invariants 17, 18, 19, 20. Create
deploy/onprem/backup/lmdb-backup.sh(run as a Coolify scheduled task / host cron / tiny sidecar sharing the LMDB volume — Coolify’s native S3 backups are database-only and do NOT cover a raw named volume, OQ-66-4). The job: (1) runmdb_copy -c /cocoindex-state/lmdb /snapshot(-ccompacts + drops free pages) —mdb_copycopies through an LMDB read transaction (MVCC read-snapshot immutable for the txn lifetime) so it produces a consistent hot snapshot with no quiesce; a naivecp/tarof livedata.mdbis NEVER used (torn-page risk). (2)tar/resticthe snapshot to the same S3 bucket Coolify targets for DB backups. Schedule: daily or twice-daily (low write volume; LMDB ≤256–512 MiB historically). Document the restore path in the B1 runbook: fetch latest S3 snapshot → untar onto the persistent volume at/cocoindex-state/lmdb→ resume the cocoindex container; fallback is memo-rebuild (inv 7). The backup path touches Supabase not at all (inv 20). NO KH application code changes. - testStrategy: A scheduled job lands an LMDB snapshot in the S3 bucket separate from Coolify’s DB backup; a snapshot taken DURING active writes restores to a consistent, openable LMDB; the documented restore path resumes the engine; the backup path touches Supabase not at all.
- dependencies: [9]
- priority: medium
{66.15} — Datapath monitor — pipeline_runs row-arrival watch
Section titled “{66.15} — Datapath monitor — pipeline_runs row-arrival watch”- id: 15
- title: Datapath monitor — pipeline_runs row-arrival watch
- description: Author a datapath monitor (
deploy/onprem/monitor/datapath-watch.tsor a scheduled query) that watchespipeline_runsrow arrival in Supabase against a stall threshold — NOT/health(which is a liveness no-op). - details: TECH change 6; TECH migration step 9 (part — the monitor; the smoke is
{66.16}). Satisfies invariants 21, 22. Createdeploy/onprem/monitor/datapath-watch.ts(or a small scheduled Supabase query). The monitor: treats “container up +/healthgreen” as insufficient (the cocoindex/healthprobe is a liveness no-op on a separate thread — green ≠ ingesting; RESEARCH §4-i,scripts/cocoindex_pipeline/server.py:131-144); queriespipeline_runsfor the most recent row (filtered to active-ingestion windows) and compares newestcreated_at/run timestamp against a configurable stall threshold (default: no terminal-status row within the expected window of a triggered ingestion → flag a datapath stall); routes the alert via Coolify notification channels (Discord/Telegram/email) or Sentry. Threshold/window values are operator-tunable and recorded in the runbook. A successful ingestion is confirmed by row arrival, not the probe. NO KH application code changes — this is a standalone monitor reading Supabase; do not modify the pipeline. - testStrategy: With containers up and
/healthgreen but nopipeline_runsrows in the expected window, the monitor flags a datapath stall (NOT “healthy”); when a real ingestion lands a row, the monitor reports healthy on row arrival, not on the probe. - dependencies: [8]
- priority: high
{66.16} — First end-to-end ingest smoke + datapath proof + ID-62 reachability hand-off
Section titled “{66.16} — First end-to-end ingest smoke + datapath proof + ID-62 reachability hand-off”- id: 16
- title: First end-to-end ingest smoke + ID-62 reachability hand-off
- description: Run a real end-to-end ingestion on the host; confirm a genuine
pipeline_runsrow lands (proving {62.1} dissolved), and confirm a host-facing HTTPS endpoint is reachable externally. This is the gating hand-off ID-62 depends on. - details: TECH change 6 + TECH migration step 9 (the smoke). THIS IS THE CROSS-TASK HAND-OFF DELIVERABLE TO ID-62 (ID-62.dependencies includes ID-66 — Task-level dep). Satisfies invariants 3, 8, 9, 21. Run a real ingestion through the co-located pipeline (
{66.8}compose,{66.9}volume,{66.10}secrets,{66.13}webhook). Confirm via ROW ARRIVAL (NOT/health): a genuinepipeline_runsrow lands in Supabase with the same shape it would have had from Cloud Run (run status, stage counts, items created) — proving the {62.1} reachability defect is DISSOLVED (inv 3): the cocoindex→pullmd hop that 403’d on Cloud Run no longer exists (host-local network). Also confirm the host-facing HTTPS ingress surface ({66.13}Coolify auto-Let’s-Encrypt) is reachable by an EXTERNAL client (e.g. a GH-hosted runner over HTTPS) — i.e. theingress:internal/403 barrier that blocked ID-62 from CI is gone. OQ-66-8 BOUNDARY (assumption, pending Liam/parent confirm): ID-66 hands ID-62 a reachable HTTPS host + proven datapath + provisioned ingress surface onto which ID-62 mounts its OWN/stageroute — ID-66 does NOT build/stage(that is KH/pipeline code, ID-62’s job, out of B1 scope per OQ-66-2). If Liam confirms ID-66 must pre-provision more (reserved hostname / Coolify app slot / publishedCOCOINDEX_FIXTURE_STAGING_URLbase), widen this acceptance. NO KH application code changes — running an ingestion through unchanged source; do not edit the pipeline. - testStrategy: A real ingestion lands a genuine
pipeline_runsrow in Supabase (proving inv 3 / {62.1} dissolved), confirmed by row arrival not/health; an external client reaches a host-facing endpoint over HTTPS (no 403). ID-62 hand-off gate met; OQ-66-8 boundary noted. - dependencies: [8, 9, 10, 11, 13, 15]
- priority: high
{66.17} — Retire cloud-run-deploy.yml + WIF + reversibility doc
Section titled “{66.17} — Retire cloud-run-deploy.yml + WIF + reversibility doc”- id: 17
- title: Retire cloud-run-deploy.yml + WIF + reversibility doc
- description: Make B1 the active deploy path: stop using
cloud-run-deploy.yml/WIF (file stays in git for reversibility), and document the one-env-var revert path + cost-floor removal in the B1 runbook. - details: TECH change 4 slice D + TECH change 8; TECH migration step 6 (retirement) + step 10 prerequisite. Satisfies invariants 16, 23, 24, 25. Only AFTER
{66.16}proves the new path works: retire.github/workflows/cloud-run-deploy.yml+ its WIF auth as the active deploy path (disable its triggers; the file + Cloud Run manifests undercloudrun/**REMAIN in git for reversibility — inv 24). WIF + deploy-SA are retired, NOT migrated. Document in the B1 runbook (docs/runbooks/onprem-b1-deploy.md): (a) cost-floor removed (inv 23) — no Filestore ($200/mo), one host vs two warm£21/mo); (b) revert path (inv 24) — cocoindex→pullmd reverts with one env var (minScale=maxScale=1Services, no per-deploy two-image Cloud Build; standing cost = the single IONOS VPS line (PULLMD_SERVICE_URL), webhook reverts with one env var (PIPELINE_RUN_WEBHOOK_URL), secrets move back to Secret Manager, deploy reverts to the WIF workflow; (c) LMDB is the single genuinely stateful artefact (inv 25) — its loss costs a rebuild, never KH data loss..ymledit → Executor MUST rungitnexus_detect_changes()before commit. NO KH application code changes. - testStrategy: A push no longer triggers
cloud-run-deploy.yml(B1 path active); the workflow +cloudrun/**manifests remain in git; the runbook documents one-env-var reverts per B1 item, the cost-floor removal (one VPS line, no Filestore/warm-×2), and the LMDB as the one stateful artefact. - dependencies: [16]
- priority: high
{66.18} — kpf dead-manifest cleanup (B1-cutover SIDE-TASK)
Section titled “{66.18} — kpf dead-manifest cleanup (B1-cutover SIDE-TASK)”- id: 18
- title: kpf dead-manifest cleanup (side-task)
- description: Standalone side-task riding the cutover: delete the two kpf Job manifests and edit the residual kpf refs in cloudrun files so
grep -rl kpf cloudrun/returns zero. Must not block or bloat the core migration. - details: TECH change 9; TECH migration step 10. Clearly-labelled standalone SIDE-TASK — NOT a core B1 step. It rides the cutover because retiring
cloud-run-deploy.yml({66.17}/inv 16) is the natural moment to drop the retired-but-still-deployed kpf tenant (RESEARCH §4-iii). Exact set (file-grounded): (1) DELETEcloudrun/jobs/prod-kpf.yaml; (2) DELETEcloudrun/jobs/staging-kpf.yaml; (3) EDITcloudrun/jobs/staging-phew.yaml— dropkpffrom the^(phew|kpf)$slug pattern in the bake-assert comment; (4) EDITcloudrun/services/prod-pullmd.yaml— update/drop the “kpf decommissioned” comment; (5) EDITcloudrun/services/staging-pullmd.yaml— same comment; (6) EDITcloudrun/services/README.md— drop the kpf SA-mapping line + the twokh-cocoindex-pipeline-kpfsmoke-verify blocks (target a Service with no manifest). Thefor TENANT in phew kpfloop incloud-run-deploy.ymlis mooted by{66.17}retiring that workflow — items 1–6 are the residual cleanup of files the retirement does not delete. NO KH application code changes — only Cloud Run YAML/README/comment edits; no.py/.tstouched. - testStrategy:
grep -rl kpf cloudrun/returns zero files (down from 6); the two kpf Job manifests are deleted; no kpf tenant is referenced by any active deploy path; no KH application code (.py/.ts) is changed. - dependencies: [17]
- priority: low
Dependency-ordering summary
Section titled “Dependency-ordering summary”{66.5} VPS stand-up + hardening ─────────────┐ ▼{66.6} Install Coolify ──────────────┬───────────────┐ │ │{66.7} GH Action build+push (ghcr) ──┤ (parallel) │ (no in-host dep — off-host build) │ ▼ │ {66.10} Secrets → Coolify env ▼ │ {66.8} Co-location compose │ deploy [6,7] │ │ │ │ ┌───────────────┘ └──────────┐ │ ▼ ▼ │ {66.9} LMDB + corpus {66.15} Datapath │ volume [8] monitor [8] │ │ │ │ │ {66.11} Coolify deploy │ │ │ webhook [6,7] │ │ │ │ │ │ │ ▼ │ │ │ {66.12} Rollback-by-tag │ │ │ [11] │ │ │ │ {66.13} Webhook │ │ │ │ repoint + DNS/SSL [8,10] ◄──┘ │ │ │ │ ▼ │ ▼ ▼ {66.14} LMDB │ ┌────────────────────┐ backup [9] └──►│ {66.16} E2E smoke + │ │ ID-62 hand-off │ │ [8,9,10,11,13,15] │ └─────────┬───────────┘ ▼ {66.17} Retire cloud-run-deploy.yml + WIF + reversibility doc [16] ▼ {66.18} kpf cleanup (side-task) [17]Parallelisation (multi-agent / multi-session safe):
- Wave 1 (sequential foundation):
{66.5}→{66.6}.{66.7}(off-host GH Action build) has no in-host dependency and can run in parallel with{66.5}/{66.6}— it only needs the repo, not the host. - Wave 2 (after
{66.6}+{66.7}):{66.8}(compose deploy) and{66.10}(secrets, needs only{66.6}) and{66.11}(deploy webhook, needs{66.6}+{66.7}) can largely proceed together;{66.8}consumes the{66.7}image. - Wave 3 (after
{66.8}):{66.9}(volume),{66.15}(monitor) parallelise — both depend only on{66.8}.{66.13}(webhook repoint) needs{66.8}+{66.10}.{66.12}(rollback) needs{66.11}.{66.14}(backup) needs{66.9}. - Wave 4 (serialised convergence):
{66.16}(E2E smoke + ID-62 hand-off) is the convergence point — it depends on{66.8, 9, 10, 11, 13, 15}. It must wait for the datapath substrate to be complete. - Wave 5 (serialised teardown):
{66.17}(retire Cloud Run path) depends on{66.16}(don’t tear down the old path until the new one is proven).{66.18}(kpf cleanup side-task) depends on{66.17}.
All dependencies are sibling-only (other Subtasks of Task 66). No cross-Task Subtask
dependency is expressed. The ID-62→ID-66 dependency is correctly a Task-level
dependency (ID-62.dependencies includes 66), satisfied by the {66.16} hand-off
deliverable — not a Subtask-level cross-Task dep.
Coverage confirmation
Section titled “Coverage confirmation”TECH Proposed changes 1–9 → Subtask mapping (all 9 covered):
| TECH change | Subtask(s) | PRODUCT invariants |
|---|---|---|
| 1 — Co-location compose | {66.8} | 1, 2, 3, 4, 11 |
| 2 — LMDB persistent volume | {66.9} | 5, 6, 7 |
| 3 — Secrets → Coolify env | {66.10} | 10, 11, 12 |
| 4 — Thin GH Action / ghcr.io / Coolify pull / retirement | {66.7} (build+push), {66.11} (webhook pull), {66.12} (rollback), {66.17} (retirement) | 13, 14, 15, 16 |
5 — mdb_copy → S3 backup | {66.14} | 17, 18, 19, 20 |
| 6 — Datapath monitor + smoke | {66.15} (monitor), {66.16} (smoke) | 21, 22 (+ 3, 8, 9 proven at smoke) |
| 7 — Webhook repoint + DNS/SSL | {66.13} | 8, 9 |
| 8 — Cost-floor removal + reversibility | {66.17} (folded with retirement) | 23, 24, 25 |
| 9 — kpf dead-manifest cleanup (side-task) | {66.18} | (in-scope-adjacent — no core invariant) |
TECH migration steps 1–10 → Subtask mapping (all 10 covered):
| Step | Subtask |
|---|---|
| 1 — VPS stand-up + hardening | {66.5} |
| 2 — Install Coolify | {66.6} |
| 3 — Co-location compose deploy | {66.8} |
| 4 — LMDB + corpus volume | {66.9} |
| 5 — Secrets migration | {66.10} |
| 6 — Thin GH Action + retirement | {66.7} + {66.11} + {66.12} + {66.17} |
| 7 — Webhook repoint + DNS/SSL | {66.13} |
| 8 — LMDB backup job | {66.14} |
| 9 — E2E smoke + datapath monitor | {66.15} + {66.16} |
| 10 — kpf cleanup | {66.18} |
All 25 PRODUCT invariants are covered via the TECH change → invariant map (carried forward from TECH’s coverage map), and all 9 TECH changes + all 10 migration steps map to at least one Subtask.
Risks and open items (carried from TECH; for parent/Liam awareness)
Section titled “Risks and open items (carried from TECH; for parent/Liam awareness)”| Item | Note |
|---|---|
| OQ-66-8 (NEW, this PLAN) | The exact ID-66→ID-62 /stage hand-off contract. Assumption adopted (host reachable + datapath proven + ingress provisioned; ID-66 does NOT build /stage). Appended to OQ-pending.md. {66.16} acceptance widens if Liam confirms more pre-provisioning is needed. |
| OQ-66-5 | (pre-known, deferred) GCP billing export to harden the cost baseline. Not blocking. |
| OQ-66-7 | (pre-known, deferred) B2 self-host-Supabase + AGPL-pullmd conveyance. Not blocking. |
| 16 GB RAM ceiling | The one dimension neither VPS exceeds (OQ-66-1). Builds run off-host ({66.7}) so the host RAM is never build-contended; watch host memory in Coolify monitoring. |
| Sidecar image-tag drift | Playwright/Trafilatura :2 floating tags — resolve :2→digest before first deploy ({66.8} details). |
Notes for the Orchestrator
Section titled “Notes for the Orchestrator”- No Subtask touches KH application code. Every record’s
detailsrepeats the guard. The only.github/workflows/*.ymledits are{66.7},{66.11},{66.17},{66.18}— the Executor should rungitnexus_detect_changes()before committing those four. {66.16}is the ID-62 gate. It is the named cross-Task hand-off deliverable; ID-62’s Task-level dependency on ID-66 is satisfied by{66.16}’s acceptance.{66.18}is a labelled side-task — keep it last; it must not block or bloat the core migration.- 14 Subtasks (
{66.5}–{66.18}) — within the 25 soft ceiling; no Task-split signal.