Skip to main content
Paul Welty, PhD AI, WORK, AND STAYING HUMAN

Work log: 2026-03-27

What shipped today

Image upload infrastructure — Cloudinary end-to-end

The day’s big push was replacing the previously-reverted Vercel Blob upload system with Cloudinary, which was already in use by the engine for DALL-E images. This eliminated a multi-day blocker (Blob store was never provisioned) and gave the whole product a consistent image storage story. The upload route (/api/upload/image) validates MIME type and size, streams to Cloudinary under authexis/user-uploads/{workspaceId}/, and returns a CDN URL. The shared ImageUploader component wraps the whole flow in a click-to-upload zone with thumbnail preview and inline error state.

With the upload primitive in place, three features landed in quick succession: featured images on content detail (replaces the static <img> with a live uploader, optimistically updates the UI), brand reference images in workspace settings (5-slot grid of Cloudinary thumbnails that feed into the AI image generation prompt as visual references), and image_url on social posts (DB column, edit panel in the queue UI, thumbnail on queue cards). The social post image work was the most complex — it required wiring image publishing into all four platform integrations. Bluesky and Mastodon got full blob-upload paths (Bluesky requires uploading the image bytes and embedding via the AT Protocol spec; Mastodon uses /api/v1/media). LinkedIn uses the Community Management API’s asset registration flow. Threads is the simplest — it accepts a public URL directly via the IMAGE media type.

Sentry + engine reliability sweep

Five Sentry-sourced issues closed today. A schema mismatch (c.style column never existed) was generating 855 errors per day — fixed with a migration and the IF NOT EXISTS guard. A dead loop in notifications.py was trying to iterate slots.articles, a field removed in an earlier refactor — one-liner fix. The MCP server was propagating ClientDisconnect as 500s — wrapped at the ASGI boundary to log at INFO and return cleanly. The claude.py service got a proper rate-limit backoff (tenacity, 5 attempts, exponential backoff on RateLimitError) plus a concurrency semaphore (asyncio.Semaphore(5)) to cap parallel Claude calls. Sentry user context was added to the engine’s process_command() so errors now show affected user counts instead of anonymous events.

A pass over eight engine files added exc_info=True to existing log calls and replaced bare swallows with log.warning(..., exc_info=True) — the fix for the widespread (No error message) entries that were making Sentry useless for Python errors.

Completed

  • #1745 Add Cloudinary upload route + shared ImageUploader component
  • #1746 Wire ImageUploader into content detail featured image section
  • #1738 Brand image style library — upload visual reference samples for AI post styling
  • #1747 Add image_url to social posts: DB column, queue UI, and platform publishing
    • #1756 DB migration + web layer for social post image attachment
    • #1757 Engine SELECT + Bluesky + Mastodon image publishing
    • #1758 LinkedIn + Threads image publishing
  • #1750 column c.style does not exist — schema mismatch breaking queries (AUTHEXIS-14)
  • #1748 BriefingSlots object has no attribute ‘articles’ (AUTHEXIS-18)
  • #1752 Escalating starlette.requests.stream error (AUTHEXIS-1C)
  • #1749 Anthropic 429 rate limit errors — add backoff and request queuing
  • #1753 Add Sentry user context so errors show affected user count
  • #1751 Add descriptive messages to Python exception handling
  • #1274 RateLimitError/429 Sentry issue — closed (fixed by #1749)
  • #1754 GitHub API 404 — closed (spurious, token/repo fine)

Release progress

  • v1.5: 49/50 closed — one backlog item remaining (#743 dashboard redesign), effectively complete
  • v2.0–v2.2: all closed (43 issues total)

Carry-over

None. Queue is empty — no ready-for-prep or ready-for-dev issues. All actionable Sentry errors addressed.

Risks

  • Supabase CLI migration sync is fragile — local and remote can drift, causing supabase db push to fail. Workaround is the Supabase MCP apply_migration tool. Upstream issue remains.
  • LinkedIn image publishing uses the Community Management API (/rest/posts) which requires the w_member_social scope. If any workspace’s LinkedIn token was issued before this scope was added, their image posts will silently fall back to text-only.

Flags and watch-outs

  • CLOUDINARY_URL was only in the engine .env — added to all three Vercel environments (production, preview, development) today. If a new environment is provisioned, remember to add it.
  • Hashtags are stored as TEXT (space-separated), not TEXT[]. Keep this consistent.

Next session

Queue is empty. Backlog candidates for the next cycle:

  • #927 — Dockerfile uses Python 3.11; dev environment likely uses 3.12+. Low risk, easy win.
  • #743 — Dashboard redesign (v1.5’s last open issue). Needs product taste call from Paul before speccing.
  • #742 — Alternative layouts. Also needs taste discussion.

Start with #927 (Dockerfile upgrade) if autonomous — it’s concrete and self-contained. For anything bigger, pull Paul in first.

Why customer tools are organized wrong

This article reveals a fundamental flaw in how customer support tools are designed—organizing by interaction type instead of by customer—and explains why this fragmentation wastes time and obscures the full picture you need to help users effectively.

Infrastructure shapes thought

The tools you build determine what kinds of thinking become possible. On infrastructure, friction, and building deliberately for thought rather than just throughput.

Server-side dashboard architecture: Why moving data fetching off the browser changes everything

How choosing server-side rendering solved security, CORS, and credential management problems I didn't know I had.

The work of being available now

A book on AI, judgment, and staying human at work.

The practice of work in progress

Practical essays on how work actually gets done.

Silence by design

Most systems have more suppression than their owners realize. It gets installed for good reasons. The cost accumulates slowly, in the form of systems you can't operate because you've removed the signals that would let you understand them.

Designed to learn, built to ignore

The most dangerous organizational failures don't throw errors. They look fine, return results, and quietly stay frozen at the moment of their creation.

The variable that was never wired in

The gap between having a solution and using a solution is one of the most persistent failure modes in organizations. You see the escaped variable. You see the risk register. You assume the work is done.