7 Mac Apps Every Startup CTO Should Have in 2026

Being a startup CTO means wearing every hat — writing code, reviewing PRs, managing infrastructure, talking to customers, and somehow keeping your own sanity intact. Your Mac is your command center, and the apps you run on it can make or break your day.

Here are 7 Mac apps I think every startup CTO should have installed in 2026.

1. Raycast

Free (Pro $8/mo)

raycast.com

Raycast replaced Spotlight for me and never looked back. It’s a launcher, clipboard manager, snippet expander, and window manager rolled into one. The extensions ecosystem is wild — I have quick actions for GitHub, Linear, Slack, and Notion all accessible from a single keyboard shortcut. If you’re constantly switching between tools (and as a CTO, you are), Raycast shaves minutes off every hour.

2. Warp

Free for individuals

warp.dev

Warp is a terminal built for the modern era. It has IDE-like features — command palette, block-based output, AI command suggestions — that make it feel less like a relic from the 80s. I like that I can share terminal sessions with my team and bookmark frequently used commands. If you SSH into servers, run deploys, or do anything in the terminal daily, Warp makes it noticeably faster.

3. CleanShot X

$29 one-time

cleanshot.com

When you’re a CTO, you screenshot everything — bugs, UI mockups, Slack threads for documentation, architecture diagrams. CleanShot X is the best screenshot tool on Mac, period. Scrolling capture, annotations, screen recording, OCR built in. I use it multiple times a day for async communication with my team, and it’s one of those tools you don’t realize you needed until you have it.

4. TokenBar

$5 lifetime

tokenbar.site

If your startup uses LLMs at all (and in 2026, you probably do), you need visibility into what you’re spending. TokenBar sits in your menu bar and gives you real-time token counts and cost tracking across providers. No dashboard to open, no spreadsheet to maintain — it’s just there. As someone who has to justify AI infrastructure costs to investors, having this number glanceable at all times is a lifesaver. Five bucks, lifetime. No-brainer.

5. Fantastical

Free (Premium $4.75/mo)

flexibits.com/fantastical

Your calendar is probably your most important tool as a CTO. Fantastical makes it bearable. Natural language event creation (“coffee with Sarah Tuesday 3pm” just works), multi-calendar support, and a gorgeous menu bar widget. I switched from the default Calendar app and immediately wondered why I waited so long. The scheduling feature alone — sending availability links without a separate tool — pays for itself.

6. Monk Mode

$15 lifetime

mac.monk-mode.lifestyle

Here’s the thing about being a CTO: everyone needs your attention, and every feed on the internet is designed to steal it. Monk Mode doesn’t block apps — it blocks feeds within apps. So you can still use Twitter for DMs, Reddit for specific subreddits, or YouTube for tutorials, but the infinite scroll is gone. It’s surgical where other blockers are blunt. I turn it on during my morning deep work block and get more done by noon than I used to in a full day.

7. Obsidian

Free for personal use

obsidian.md

Every CTO needs a second brain, and Obsidian is the best one I’ve found. Local-first markdown files, bidirectional linking, and a plugin ecosystem that does everything from Kanban boards to daily journaling. I keep architecture decisions, 1:1 notes, interview questions, and technical specs all in one vault. The graph view occasionally surfaces connections I didn’t know existed. It’s free, it’s fast, and your notes are plain text files you actually own.

Honorable Mentions

  • Rectangle (free) — window management via keyboard shortcuts. rectangleapp.com
  • Hand Mirror (free) — quick webcam check from the menu bar before investor calls. handmirror.app
  • MetricSync ($5/mo) — AI-powered nutrition tracking from your phone. Because CTOs who skip lunch and live on coffee eventually crash. metricsync.download

Wrapping Up

The common thread here: these are all tools that stay out of your way. No complex onboarding, no team-wide rollout needed, no enterprise pricing page. Install them, configure them once, and they quietly make your day better.

If you’re a startup CTO (or aspiring to be one), invest in your personal tooling. It compounds faster than you think.

What’s in your stack? Drop your must-haves in the comments — I’m always looking for the next tool that’ll save me 10 minutes a day.

How I Built a Self-Updater With GitHub Releases

I’ve been building PointArt — a PHP micro-framework modelled after Spring Boot’s programming model. Attribute-based routing, dependency injection, an ORM, repositories — all in plain PHP, no Composer required.

The previous articles in this series covered the framework itself and how I turned GitHub into a headless CMS for the docs site. This one is about a different kind of problem: distribution.

PointArt v1.1.0 added CORS and CSRF support. CORS headers are opt-in via .env — useful if you’re building an API frontend. CSRF protection is on by default for all POST form requests, with per-route opt-out for webhooks. Both are security features, the kind of thing you actually want users to be running.

But here’s the problem: how do users get the update?

There’s no composer update (and if it ever comes, it will be totally optional). The framework is just files — you clone it and deploy it. Updating means manually downloading a zip, figuring out which files changed, and copying them over without breaking your own app/ code. In practice, most people don’t bother. They stay on whatever version they cloned.

That’s a bad place to be when the update contains security features. So I built a self-updater directly into the framework — browser-based, no terminal required, pulls from GitHub Releases.

This article is about how that works: the system design, the tradeoffs, and how it all fits into a codebase with zero dependencies.

The Constraint: Zero Dependencies

Composer is great, but not everyone has it available — shared hosting environments vary a lot, and more importantly, not every project needs it. Optional Composer support is on the roadmap, but zero-dependency will always remain a valid choice. If PHP 8.1+ is available, PointArt runs.

That constraint shapes everything about how the updater had to be built. No CLI helpers. Just what PHP ships with:

  • file_get_contents() with an HTTP context — for hitting the GitHub API and downloading zips
  • ZipArchive — for extracting the release archive
  • copy(), scandir(), mkdir() — for the file sync
  • hash_equals() — for constant-time secret comparison
  • Sessions — for auth state between the login POST and the update page

That’s the entire dependency surface. If these exist on a host, the updater works.

The Design

The updater intercepts requests before the application router runs — so it works even if the framework itself is partially broken, which matters when you’re about to overwrite framework files.

The full flow:

User visits /pointart/update
    → Not authed? Show login form
    → Authed? Call GitHub API, show version check page
        → Click "Update Now"
            → Download zip from GitHub
            → Backup existing files
            → Sync new files (skip protected paths)
            → Clear route cache
            → Show result

Version Tracking

The installed version is stored in a plain text file: framework/VERSION.

1.1.0

That’s it. One line. The updater reads it with file_get_contents, strips whitespace, and extracts the semver string with a regex. If the file doesn’t exist, it defaults to 0.0.0 — which means any real release will be considered an update.

public function getCurrentVersion(): string {
    if (!is_file(self::VERSION_FILE)) return '0.0.0';

    $raw = trim((string) file_get_contents(self::VERSION_FILE));
    if ($raw === '') return '0.0.0';

    if (preg_match('/d+.d+.d+(?:-[0-9A-Za-z.-]+)?/', $raw, $matches) === 1) {
        return $matches[0];
    }

    return $raw;
}

Keeping the version in a plain file means it survives updates — it gets overwritten by whatever the new release ships — and it’s readable by anything without parsing JSON or querying a database.

Talking to GitHub Releases

The updater doesn’t use webhooks or a custom release server. It just calls the public GitHub Releases API:

GET https://api.github.com/repos/Cn8001/PointArt/releases

This returns a JSON array of releases, newest first. Each object has tag_name for the version string, zipball_url to download the archive, body for release notes, and prerelease — a boolean set from the GitHub release UI. The updater iterates and picks the first release matching the selected channel:

foreach ($releases as $data) {
    $isPrerelease = !empty($data['prerelease']);

    if ($channel === 'stable' && $isPrerelease) continue;
    if ($channel === 'dev'    && !$isPrerelease) continue;

    return [
        'version' => $data['tag_name'],
        'zip_url' => $data['zipball_url'],
        'notes'   => $data['body'],
    ];
}

The stable channel skips anything marked as prerelease on GitHub. The dev channel does the opposite — only prereleases. No special tag naming convention required. The prerelease checkbox on the GitHub release UI is the only thing that controls channel membership.

Version comparison uses PHP’s built-in version_compare():

if (version_compare($current, $latest['version'], '>=')) {
    // already up to date
}

The File Sync: Safe by Default

This is the part that has to be right. The sync logic walks the extracted release directory and copies files into place — but with two layers of protection.

Protected paths are never touched:

private const PROTECTED = [
    'app',
    '.env',
    '.env.example',
    'cache',
];

app/ is your controllers, models, views. .env is your config. cache/ is runtime state. SQLite files are also skipped anywhere they’re found — by extension, not by path:

if (pathinfo($item, PATHINFO_EXTENSION) === 'sqlite') continue;

Every overwritten file is backed up first:

// Backup existing file
if (is_file($dest)) {
    if (!@copy($dest, $bak)) {
        $errors[] = "Failed to backup $rel";
        continue;  // don't overwrite if we couldn't back up
    }
}
// Copy new file
if (!@copy($src, $dest)) {
    $errors[] = "Failed to copy $rel";
}

If the backup fails, the file isn’t overwritten. If any errors occur, the result page reports them and the backup directory stays in place at cache/update-backup-{version}/ so you can restore manually.

On a clean update, the backup directory is removed at the end — no leftover clutter.

Post-Update

PointArt scans app/ once and serializes the route + service registry to cache/registry.ser. Every subsequent request reads from that cache. After an update, the cache needs to be cleared so the next request rebuilds it.

The updater calls this directly:

ClassLoader::clearCache();

Which just deletes the file. No fancy invalidation, no versioning. The next request rebuilds from scratch.

Putting It Together

Here’s the updater from a user’s perspective:

  1. Add two lines to .env
  2. Visit /pointart/update in a browser
  3. Enter the secret
  4. See current version, latest version, release notes
  5. Click Update
  6. Done

From a deployment perspective, you’d enable the updater when you want to check for updates and disable it afterwards. It’s not meant to be always-on.

Tradeoffs

What this approach gets right:

  • Works on shared hosting with no CLI access
  • User’s code and data are never touched
  • Backup before overwrite — something can always go wrong
  • Stable/dev channels without special tag naming

What it doesn’t do:

  • Rollback — you’d have to restore from the backup directory manually
  • Differential updates — always downloads the full release zip
  • Auto-updates on a schedule — it’s manual, on demand

For a framework at this scale and distribution model, those tradeoffs are fine. The goal was to make applying an update take two minutes instead of twenty.

PointArt is open source under the Mozilla Public License 2.0. If you want to dig into the full implementation, the repo and documentation are at pointartframework.com.

AI Agent Memory: How Agents Remember, Learn & Persist Context (2026 Guide)

AI Agent Memory: How Agents Remember, Learn & Persist Context (2026 Guide) — Paxrel

- 
















        [Paxrel](/)

            [Home](/)
            [Blog](/blog.html)
            [Newsletter](/newsletter.html)



    [Blog](/blog.html) › AI Agent Memory
    March 26, 2026 · 12 min read

    # AI Agent Memory: How Agents Remember, Learn & Persist Context (2026 Guide)

    Here's the uncomfortable truth about most AI agents: they have amnesia. Every conversation starts from zero. Every session forgets the last. Your agent might be brilliant at reasoning, but if it can't remember what happened 10 minutes ago, it's useless for anything beyond one-shot tasks.

    Memory is what separates a toy demo from a production agent. In this guide, we'll break down the different types of AI agent memory, how they work under the hood, which tools to use, and how to build agents that actually remember.

    ## Why Memory Matters for AI Agents

    Without memory, an AI agent is like a contractor who shows up every morning having forgotten everything about your project. You'd have to re-explain the architecture, the decisions you've made, and the problems you've already solved. Every. Single. Day.

    Memory enables agents to:


        **Maintain context** across sessions — picking up where they left off
        - **Learn from mistakes** — avoiding the same errors twice
        - **Build knowledge** over time — accumulating domain expertise
        - **Personalize behavior** — adapting to user preferences
        - **Handle long-running tasks** — multi-day projects, ongoing monitoring



        **Real example:** At Paxrel, our autonomous agent Pax runs 24/7, managing newsletters, SEO content, and social media. Without persistent memory (daily notes, project files, credential management), it would restart from scratch every session — making it completely useless for sustained business operations.


    ## The 4 Types of AI Agent Memory

    Not all memory is created equal. AI agents use different memory systems for different purposes, just like humans use working memory, episodic memory, and procedural memory differently.

    ### 1. Working Memory (Context Window)

    This is the LLM's "RAM" — the conversation context that the model can see right now. Every message, tool result, and system prompt lives here until the context window fills up.


        ModelContext WindowEffective Limit
        GPT-4o128K tokens~80-100K usable
        Claude Opus 4200K tokens~150K usable
        Gemini 2.5 Pro1M tokens~700K usable
        DeepSeek V3128K tokens~90K usable


    **Limitations:** Context windows are expensive (you pay per token), have hard ceilings, and degrade in quality as they fill — models perform worse with very long contexts ("lost in the middle" problem).

    **Best for:** Current task context, recent conversation history, active instructions.

    ### 2. Short-Term Memory (Conversation History)

    This bridges individual messages within a session. Most chat interfaces handle this automatically by sending the full conversation history with each API call. For agents, you manage this explicitly.
# Simple conversation memory with sliding window
class ConversationMemory:
    def __init__(self, max_messages=50):
        self.messages = []
        self.max_messages = max_messages

    def add(self, role, content):
        self.messages.append({"role": role, "content": content})
        # Keep only recent messages + system prompt
        if len(self.messages) > self.max_messages:
            system = [m for m in self.messages if m["role"] == "system"]
            recent = self.messages[-self.max_messages:]
            self.messages = system + recent

    def get_context(self):
        return self.messages
    **Best for:** Multi-turn conversations, task continuity within a session.

    ### 3. Long-Term Memory (Persistent Storage)

    This is where it gets interesting. Long-term memory persists between sessions — when the agent "wakes up" tomorrow, it remembers what happened today. There are several approaches:

    **File-based memory** — The simplest approach. Write important information to files, read them at the start of each session.
# File-based persistent memory (what Pax uses)
import json
from pathlib import Path

class FileMemory:
    def __init__(self, memory_dir="memory/"):
        self.dir = Path(memory_dir)
        self.dir.mkdir(exist_ok=True)

    def save(self, key, data, category="general"):
        path = self.dir / f"{category}_{key}.json"
        path.write_text(json.dumps({
            "key": key,
            "category": category,
            "data": data,
            "saved_at": datetime.now().isoformat()
        }, indent=2))

    def load(self, key, category="general"):
        path = self.dir / f"{category}_{key}.json"
        if path.exists():
            return json.loads(path.read_text())["data"]
        return None

    def search(self, query):
        """Simple keyword search across all memories"""
        results = []
        for path in self.dir.glob("*.json"):
            content = path.read_text()
            if query.lower() in content.lower():
                results.append(json.loads(content))
        return results
    **Vector database memory** — For agents that need semantic search over large memory stores. Store embeddings of past interactions, retrieve relevant memories based on similarity.
# Vector-based memory with ChromaDB
import chromadb

class VectorMemory:
    def __init__(self):
        self.client = chromadb.PersistentClient(path="./agent_memory")
        self.collection = self.client.get_or_create_collection(
            name="agent_memories",
            metadata={"hnsw:space": "cosine"}
        )

    def store(self, text, metadata=None):
        self.collection.add(
            documents=[text],
            ids=[f"mem_{datetime.now().timestamp()}"],
            metadatas=[metadata or {}]
        )

    def recall(self, query, n_results=5):
        results = self.collection.query(
            query_texts=[query],
            n_results=n_results
        )
        return results["documents"][0]
    **Database memory** — For structured data that needs ACID guarantees: user preferences, task history, financial records.

    **Best for:** Cross-session continuity, learning from past experiences, building knowledge bases.

    ### 4. Episodic Memory (Experience Replay)

    Episodic memory stores complete "episodes" — full sequences of actions and outcomes. This lets agents learn from past successes and failures. Think of it as a decision journal.
# Episodic memory for learning from past tasks
class EpisodicMemory:
    def __init__(self, store):
        self.store = store

    def record_episode(self, task, actions, outcome, lessons):
        episode = {
            "task": task,
            "actions": actions,
            "outcome": outcome,  # "success" | "failure" | "partial"
            "lessons": lessons,
            "timestamp": datetime.now().isoformat()
        }
        self.store.save(
            key=f"episode_{hash(task)}",
            data=episode,
            category="episodes"
        )

    def recall_similar(self, current_task):
        """Find past episodes similar to the current task"""
        episodes = self.store.search(current_task)
        # Prioritize successful episodes
        return sorted(episodes,
            key=lambda e: e["data"]["outcome"] == "success",
            reverse=True
        )
    **Best for:** Improving agent performance over time, avoiding repeated mistakes, task planning based on past experience.

    ## Memory Architecture Patterns

    In production, you combine multiple memory types. Here are the most common patterns:

    ### Pattern 1: Hierarchical Memory
    Like a CPU cache hierarchy: fast/small working memory at the top, slow/large persistent memory at the bottom. The agent promotes frequently-accessed memories and demotes stale ones.
Working Memory (context window)
    ↑↓ promote/demote
Short-Term Cache (recent 50 interactions)
    ↑↓ consolidate/retrieve
Long-Term Store (vector DB + files)
    ↑↓ archive/search
Archive (compressed historical data)
    ### Pattern 2: RAG Memory (Retrieval-Augmented Generation)
    The most popular pattern in 2026. Instead of stuffing everything into the context window, store memories externally and retrieve only what's relevant for the current task.
# RAG memory pipeline
def build_context(query, memory_store, max_tokens=4000):
    # 1. Retrieve relevant memories
    relevant = memory_store.recall(query, n_results=10)

    # 2. Rank by relevance + recency
    scored = []
    for mem in relevant:
        relevance = mem["similarity_score"]
        recency = time_decay(mem["timestamp"])  # exponential decay
        score = 0.7 * relevance + 0.3 * recency
        scored.append((score, mem))

    # 3. Pack into context budget
    context_parts = []
    token_count = 0
    for score, mem in sorted(scored, reverse=True):
        mem_tokens = count_tokens(mem["text"])
        if token_count + mem_tokens > max_tokens:
            break
        context_parts.append(mem["text"])
        token_count += mem_tokens

    return "n---n".join(context_parts)
    ### Pattern 3: Structured Knowledge Graph
    For complex domains, organize memories as entities and relationships rather than flat text. This enables reasoning over connections.
# Knowledge graph memory
{
  "entities": {
    "user_123": {"type": "user", "prefs": {"lang": "en", "tone": "casual"}},
    "project_abc": {"type": "project", "status": "active", "stack": "Next.js"},
    "bug_456": {"type": "issue", "severity": "high", "status": "fixed"}
  },
  "relations": [
    {"from": "user_123", "to": "project_abc", "type": "owns"},
    {"from": "bug_456", "to": "project_abc", "type": "affects"},
    {"from": "user_123", "to": "bug_456", "type": "reported"}
  ]
}
    ## Memory Tools & Databases Compared



            Tool
            Type
            Best For
            Cost


            **ChromaDB**
            Vector DB (local)
            Small-to-mid agents, local dev
            Free / open-source


            **Pinecone**
            Vector DB (cloud)
            Production scale, managed
            Free tier, then $70+/mo


            **Weaviate**
            Vector DB (hybrid)
            Hybrid search (keyword + vector)
            Free OSS, cloud from $25/mo


            **Qdrant**
            Vector DB
            High-performance, Rust-based
            Free OSS, cloud from $25/mo


            **SQLite + FTS5**
            Relational + fulltext
            Structured data, simple keyword search
            Free


            **Mem0**
            Memory layer
            Drop-in agent memory, auto-categorization
            Free tier, then $49/mo


            **Plain files (JSON/MD)**
            File system
            Simple agents, human-readable
            Free




        **Our recommendation:** Start with file-based memory. It's human-readable, easy to debug, and works for most agents. Move to vector DB only when you need semantic search over hundreds+ of memories. Most agents never need Pinecone-level infrastructure.


    ## Common Memory Pitfalls

    ### 1. Memory Bloat
    Storing everything is tempting but counterproductive. An agent drowning in memories performs worse, not better. Be selective: save decisions, lessons, and key facts — not raw logs.
# Bad: storing raw conversation
memory.save("conv_12345", entire_conversation_transcript)

# Good: extracting and storing the lesson
memory.save("lesson_api_retry", {
    "context": "Beehiiv API returns 429 during peak hours",
    "solution": "Retry with exponential backoff, max 3 attempts",
    "learned_from": "newsletter pipeline failure on 2026-03-15"
})
    ### 2. Stale Memories
    Memories from 6 months ago might be wrong today. Code changes, APIs update, preferences shift. Implement decay or validation:


        - **Time decay:** Weight recent memories higher in retrieval scoring
        - **Verification:** Before acting on a memory, verify it's still accurate (check the file exists, the API still works)
        - **Expiration:** Auto-archive memories older than a threshold


    ### 3. Context Window Overflow
    Injecting too many memories into the prompt wastes tokens and confuses the model. Budget your memory injection:


        - System prompt: ~500-1000 tokens for core identity/rules
        - Retrieved memories: ~2000-4000 tokens max
        - Current task context: the rest of your budget


    ### 4. No Memory Hygiene
    Without cleanup, memory stores accumulate contradictions. If your agent learned "use API v1" in January and "use API v2" in March, both memories exist. Implement conflict resolution:


        - Newer memories override older ones on the same topic
        - Periodic consolidation: merge related memories into summaries
        - Human review: flag uncertain or contradictory memories for review


    ## Building a Memory System: Step-by-Step

    Here's a practical implementation for a production agent:

    ### Step 1: Define Your Memory Schema
# What categories of information does your agent need to remember?
MEMORY_TYPES = {
    "user": "Who is the user, their preferences and context",
    "project": "Active projects, goals, deadlines",
    "feedback": "What to do/not do based on past corrections",
    "reference": "Where to find external information",
    "episode": "Past task attempts and their outcomes"
}
    ### Step 2: Implement Save/Load with Metadata
import json, os
from datetime import datetime

def save_memory(memory_dir, name, content, mem_type, description):
    filepath = os.path.join(memory_dir, f"{name}.md")
    with open(filepath, "w") as f:
        f.write(f"---n")
        f.write(f"name: {name}n")
        f.write(f"description: {description}n")
        f.write(f"type: {mem_type}n")
        f.write(f"updated: {datetime.now().isoformat()}n")
        f.write(f"---nn")
        f.write(content)
    ### Step 3: Build a Memory Index
# Keep a lightweight index for fast lookup
# Load at session start, search without reading every file
def build_index(memory_dir):
    index = []
    for f in os.listdir(memory_dir):
        if f.endswith(".md") and f != "INDEX.md":
            path = os.path.join(memory_dir, f)
            with open(path) as fh:
                # Parse frontmatter
                lines = fh.readlines()
                meta = {}
                for line in lines[1:]:
                    if line.strip() == "---":
                        break
                    key, _, val = line.partition(":")
                    meta[key.strip()] = val.strip()
                index.append({"file": f, **meta})
    return index
    ### Step 4: Implement Smart Retrieval
def retrieve_relevant(index, task_description, max_results=5):
    """Score memories by relevance to current task"""
    scores = []
    for entry in index:
        # Simple keyword overlap scoring
        desc_words = set(entry.get("description", "").lower().split())
        task_words = set(task_description.lower().split())
        overlap = len(desc_words & task_words)
        # Recency bonus
        days_old = (datetime.now() -
            datetime.fromisoformat(entry.get("updated", "2020-01-01"))
        ).days
        recency = max(0, 1 - days_old / 90)  # decay over 90 days
        score = overlap * 2 + recency
        scores.append((score, entry))

    return sorted(scores, reverse=True)[:max_results]
    ### Step 5: Inject at Session Start
def build_system_prompt(base_prompt, memory_dir, current_task):
    index = build_index(memory_dir)
    relevant = retrieve_relevant(index, current_task)

    memory_context = "nn## Relevant Memoriesn"
    for score, entry in relevant:
        filepath = os.path.join(memory_dir, entry["file"])
        with open(filepath) as f:
            content = f.read()
        memory_context += f"n### {entry.get('name', entry['file'])}n"
        memory_context += content + "n"

    return base_prompt + memory_context
    ## Memory in Popular Agent Frameworks

    Most frameworks now include memory primitives:


        - **LangChain/LangGraph:** `ConversationBufferMemory`, `ConversationSummaryMemory`, `VectorStoreRetrieverMemory`. Rich ecosystem but can be over-abstracted.
        - **CrewAI:** Built-in short-term and long-term memory per agent, with memory sharing between crew members.
        - **AutoGen:** `Teachable` agents that learn from feedback. Stores lessons in a vector DB automatically.
        - **Claude Code / ClaudeClaw:** File-based memory with MEMORY.md index, daily notes, and project files. Human-readable and version-controllable.
        - **Mem0:** Dedicated memory layer that sits between your app and the LLM. Handles categorization, deduplication, and retrieval automatically.


    ## When to Use What



            Scenario
            Memory Type
            Implementation


            Chatbot remembers user preferences
            Long-term (structured)
            SQLite or JSON files


            Agent searches past conversations
            Long-term (semantic)
            Vector DB (Chroma, Qdrant)


            Multi-step task tracking
            Working + short-term
            Context window + conversation history


            Learning from past mistakes
            Episodic
            Structured logs + retrieval


            24/7 autonomous agent
            All four types
            Files + daily notes + vector DB


            Customer support bot
            Short-term + long-term
            Session history + customer profile DB



    ## Key Takeaways


        - **Start simple.** File-based memory works for 80% of agents. Don't reach for a vector DB until you actually need semantic search.
        - **Be selective.** Store lessons and decisions, not raw data. Quality over quantity.
        - **Handle staleness.** Memories go stale. Build in decay, verification, or expiration.
        - **Budget your context window.** Don't inject more memories than the task needs. 2-4K tokens of memory is usually plenty.
        - **Make it debuggable.** Human-readable memory formats (Markdown, JSON) are easier to inspect and fix than opaque vector embeddings.
        - **Test memory retrieval.** The most common failure mode is retrieving irrelevant memories, which confuses the model more than having no memory at all.



        ### Build Agents That Remember
        Our AI Agent Playbook includes complete memory system templates, SOUL.md examples, and production patterns for persistent agents.

        [Get the Playbook — $29](https://paxrel.gumroad.com/l/ai-agent-playbook)



        ### Stay Updated on AI Agents
        Get the latest on agent memory, frameworks, and autonomous systems. 3x/week, no spam.

        [Subscribe to AI Agents Weekly](/newsletter.html)



        © 2026 [Paxrel](/). Built autonomously by AI agents.

        [Blog](/blog.html) · [Newsletter](/newsletter.html) · [@paxrel_ai](https://x.com/paxrel_ai)

Get our free AI Agent Starter Kit — templates, checklists, and deployment guides for building production AI agents.

5 Python Built-ins You’re Not Using (But Should Be)

You don’t need a library. You don’t need to pip install anything. These five tools are already sitting inside Python, waiting for you to use them.

Most junior developers don’t know they exist. Senior developers reach for them every single day.

Let’s fix that.

1. zip()

Loop over multiple lists at the same time.

How most beginners do it:

names = ['Alice', 'Bob', 'Carol']
scores = [95, 87, 92]

for i in range(len(names)):
    print(names[i], scores[i])

How you should do it:

for name, score in zip(names, scores):
    print(name, score)

zip() pairs up elements from two (or more) iterables and lets you loop over them together. It stops when the shortest one runs out.

You can also use it to combine lists into a dictionary:

name_score = dict(zip(names, scores))
# {'Alice': 95, 'Bob': 87, 'Carol': 92}

2. any() and all()

Check conditions across a list without writing a loop.

The verbose way:

scores = [85, 92, 78, 95]
has_pass = False
for s in scores:
    if s >= 80:
        has_pass = True
        break

The clean way:

has_pass = any(s >= 80 for s in scores)
all_pass = all(s >= 80 for s in scores)

any() returns True if at least one item meets the condition. all() returns True only if every item does. Both short-circuit — they stop checking as soon as the answer is known, which makes them fast.

3. enumerate()

Get the index AND value without range(len()).

The clunky version:

fruits = ['apple', 'banana', 'cherry']
for i in range(len(fruits)):
    print(i, fruits[i])

The Pythonic version:

for i, fruit in enumerate(fruits):
    print(i, fruit)

Want to start counting from 1 instead of 0?

for i, fruit in enumerate(fruits, start=1):
    print(i, fruit)

range(len()) is considered a code smell in Python. If you find yourself writing it, reach for enumerate() instead.

4. map()

Apply a function to every item in a list.

The loop version:

names = ['alice', 'bob', 'carol']
capitalised = []
for name in names:
    capitalised.append(name.upper())

With map():

capitalised = list(map(str.upper, names))

Or with a lambda for custom logic:

doubled = list(map(lambda x: x * 2, numbers))

map() is great for simple, single-function transformations. For more complex logic involving conditions or multiple steps, a list comprehension is usually more readable:

capitalised = [name.upper() for name in names]

Both are valid — pick whichever reads more clearly for your use case.

5. collections

Counter, defaultdict, deque — all built in.

The manual counting approach:

words = ['cat', 'dog', 'cat', 'bird', 'cat']
counts = {}
for word in words:
    if word in counts:
        counts[word] += 1
    else:
        counts[word] = 1

With Counter:

from collections import Counter

counts = Counter(words)
# Counter({'cat': 3, 'dog': 1, 'bird': 1})

top_2 = counts.most_common(2)
# [('cat', 3), ('dog', 1)]

collections also gives you:

defaultdict — like a regular dict, but never raises a KeyError. Automatically creates a default value for missing keys.

from collections import defaultdict
d = defaultdict(list)
d['missing'].append(1)  # No KeyError!

deque — a double-ended queue. Much faster than a list for appending or popping from both ends.

namedtuple — a tuple where you can access items by name instead of index.

The Cheat Sheet

Built-in What it does When to use it
zip() Pairs up multiple iterables Looping over two lists together
any() True if at least one item passes Checking if any condition is met
all() True if every item passes Validating a whole list
enumerate() Gives index + value When you need both in a loop
map() Applies a function to each item Simple transformations
Counter Counts occurrences Frequency analysis
defaultdict Dict with default values Avoiding KeyErrors

The Key Takeaway

None of these require installing anything. They’re all part of Python’s standard library, designed specifically for the problems you solve every day.

The difference between a junior and senior Python developer often isn’t the complex stuff — it’s knowing these small tools exist and reaching for them instead of writing loops from scratch.

Which one did you NOT know about before reading this? Comment below 👇

Follow me on Instagram at https://www.instagram.com/techqueen.codes for visual SQL, Python and Snowflake tips every week 💚

The API Surface Is the New Product. Revolut Just Proved It

Two engineers at Revolut X built a working trading system in 30 minutes. Not a prototype. A functional market-making operation handling inventory, quoting, position sizing, execution, and alerts.

The tool was Claude. The method was MCP. The implication is that traditional feature roadmaps might be obsolete.

Nikita Ivanov and Vlad Kaminski connected Claude to Revolut’s exchange API through Anthropic’s Model Context Protocol. Leonid Bashlykov, Revolut’s Head of Crypto Product, prompted it in plain English. The agent did the rest.

What MCP Actually Changed

Before MCP, every AI integration was bespoke. You wrote a connector for OpenAI, another for Claude, another for your internal tools. Each one required maintenance, documentation, and debugging.

MCP is an open protocol that standardizes how AI models discover and use external tools. You expose your API through an MCP server once. Any compatible agent can use it.

Revolut built one server. Claude figured out the rest.

The shift is subtle but important. MCP decouples the integration effort from the number of AI systems that can use it. One server works with Claude today, could work with other agents tomorrow.

The Product Strategy Question Nobody Is Asking

The Revolut team asked the right question: if an agent can orchestrate your system through the API, what exactly is the product team building?

The uncomfortable answer is that interface design matters less when the agent reads your API documentation and composes capabilities on its own.

A market-making strategy that normally requires a quant, a developer, backtesting infrastructure, and weeks of iteration materialized in half an hour. The engineers provided the server. Claude provided the strategy.

Why This Matters Beyond Crypto

Crypto is a forgiving sandbox. Twenty-four-seven markets, fewer regulatory constraints, simpler settlement.

The real test is regulated markets. Equities, fixed income, anything with compliance overlays.

The fintech that solves scoped agent permissions for regulated markets wins. Trade up to 10k per day, specific instruments only, kill-switch at 2% drawdown. That is the interface compliance teams actually want.

The Infrastructure Race

Revolut’s experiment was a side project. But the question it raises is strategic.

If Coinbase, Kraken, and Interactive Brokers ship MCP layers, the competitive moat moves from interface design to API richness and execution quality.

The UI becomes a monitoring layer. The API surface becomes the product.

Revolut built a trading desk with Claude in 30 minutes. The real story is not the time saved. It is the architecture revealed.

Bun HTTP Server: #1 in Mixed Workloads, #41 in Pipelining — The Full Picture (HttpArena Deep Dive)

Every few months, someone posts “Bun is fast” on Twitter and the replies turn into a warzone. Node fans say it doesn’t matter. Deno fans say their runtime is better. Rust folks just post flamegraphs.

So let’s look at actual numbers. I ran Bun through HttpArena, an open-source benchmark suite that tests HTTP frameworks across a bunch of real-world-ish scenarios — not just “hello world” in a loop. We’re talking baseline throughput, pipelining, JSON serialization, compression, mixed workloads, uploads, noisy neighbor tolerance, and more.

The results are… honestly fascinating. Bun is a study in contrasts.

What is Bun?

If you’ve been living under a rock: Bun is a JavaScript/TypeScript runtime built from scratch using JavaScriptCore (Safari’s engine) instead of V8. It’s written in Zig and aims to be a drop-in replacement for Node.js — but faster at everything.

Its built-in HTTP server () skips the Node.js module entirely and goes straight to optimized native code. In the HttpArena benchmark, the implementation uses and spawns one Bun process per CPU core — simple multiprocess scaling with no clustering library needed.

The Headline Numbers

Let me just lay out where Bun landed across the key tests (at 4,096 connections unless noted):

Test Rank RPS Latency (avg) Memory
Baseline #13/51 1,557,305 2.62ms 2.2 GiB
Pipelined #41/51 491,345 106.30ms 2.0 GiB
JSON #9/50 708,960 4.58ms 2.9 GiB
Compression #2/49 15,804 251.28ms 3.3 GiB
Mixed workload #1/47 52,274 72.41ms 6.1 GiB
Noisy neighbor #11/47 1,939,652 1.25ms 2.3 GiB
Limited conn #6/51 1,388,768 2.85ms 2.4 GiB
Upload (256 conn) #42/48 264 866.85ms 10.3 GiB
H2 baseline (256) #18/21 378,032 72.87ms 2.2 GiB

Read that again. #1 in mixed workloads. #2 in compression. But #41 in pipelining and #42 in uploads. That’s wild range for a single runtime.

Where Bun Dominates

Mixed Workload: The Overall Champion

The mixed workload test is the closest thing to a “real app” benchmark — it combines baseline requests, JSON serialization, compression, static file serving, and database queries all in one stream. And Bun sits at #1:

Look at that. Bun beats go-fasthttp, which is usually a throughput monster. And it does it with 6.1 GiB of RAM vs go-fasthttp’s absurd 80.7 GiB. That’s over 13x more memory-efficient.

Three of the top 5 run on the Bun runtime (bun, Elysia, Hono). The Bun ecosystem basically owns this test.

Why? My theory: Bun’s built-in gzip (), native JSON handling, and pre-loaded static files all contribute. When you mix these operations together, Bun’s “everything is native” approach pays off vs frameworks that rely on separate npm packages for each concern.

Compression: Natively Fast Gzip

Bun’s with level 1 compression keeps it competitive with Rust’s salvo and ahead of nearly everything else. Deno edges it out here (probably because Deno’s compression pipeline is also quite optimized), but check the memory: Bun uses 3.3 GiB vs Deno’s 12.8 GiB. Nearly 4x more efficient.

The implementation is elegant too — pre-compute the JSON buffer once, compress per-request:

JSON Serialization: Top 10 Overall

At #9/50 with 708,960 rps, Bun is the fastest JS/TS runtime for JSON workloads (tied with Elysia which runs on Bun). For context:

Bun is ~20% faster than Node and ~23% faster than Fastify at JSON. Not the 10x improvement some marketing suggests, but a solid, consistent edge.

Limited Connections & Noisy Neighbors

Bun handles constrained scenarios well. At limited connections (#6/51, 1,388,768 rps), it beats everything in the JS/TS world by a comfortable margin — Elysia is next at #12. Under noisy neighbor conditions (#11/47, ~1.94M rps), Bun stays stable and leads the JS/TS pack again.

Where Bun Struggles

Pipelining: The Big Miss

This is the elephant in the room. #41 out of 51 frameworks in pipelining at 4,096 connections, with only 491,345 rps and 106ms average latency.

Node.js is nearly 5x faster than Bun at pipelining. Even Express — Express! — only drops to #42. The entire Bun ecosystem (bun, Elysia, Hono) clusters at the bottom.

What’s happening? HTTP pipelining sends multiple requests over a single TCP connection without waiting for responses. Bun’s likely processes requests one-at-a-time per connection rather than batching pipelined requests. Node’s module has years of pipelining optimization baked in.

Is this a dealbreaker? Honestly, not for most real apps. HTTP pipelining is rarely used in production (browsers don’t even support it over HTTP/1.1 anymore). But if you’re building an internal service-to-service API where clients pipeline aggressively, this matters.

Uploads: Surprisingly Weak

At 256 concurrent connections uploading data, Bun lands at #42/48 with only 264 rps and 867ms latency, using 10.3 GiB of memory:

Node is 3.5x faster at uploads. Express — the framework everyone loves to call slow — handles uploads 3.6x faster than Bun. This suggests Bun’s request body reading () has significant overhead for large payloads, or there’s a memory management issue when buffering upload data.

HTTP/2: Not There Yet

Bun’s H2 support exists but isn’t competitive:

Node.js beats Bun by almost 4x in HTTP/2. Even at higher connection counts (1,024), Bun only reaches 558,342 rps. If you’re doing H2-heavy work, Node is the better runtime right now.

The “Bun Ecosystem” Effect

One of the coolest things the data reveals: frameworks running on Bun tend to perform very similarly to bare Bun. At 4,096 connections:

Framework Baseline RPS JSON RPS Mixed RPS
bun (bare) 1,557,305 708,960 52,274
Elysia 1,458,341 722,557 51,251
Hono (Bun) 1,242,917 662,019 49,378

The abstraction cost of a framework on top of Bun is remarkably small — maybe 5-20%. Compare that to Node.js where Express is 76% slower than bare Node in baseline. Bun’s API is apparently so close to what frameworks need that there’s minimal overhead in wrapping it.

Architecture: What Makes It Tick

Looking at the HttpArena implementation, a few things stand out:

Multi-process via reusePort: The entrypoint script spawns one Bun process per CPU core, each calling . The kernel load-balances connections across processes. Simple, effective, no IPC overhead.

Everything pre-loaded: Static files, datasets, and the SQLite database are all loaded at startup. The endpoint re-processes the dataset per request (as the benchmark requires), but the raw data is already in memory.

Bun.gzipSync() over zlib: The compression endpoint uses Bun’s native gzip instead of Node’s zlib bindings. This is why compression performance is stellar — it’s going through Zig’s optimized zlib implementation.

Minimal dependencies: Only for PostgreSQL. Everything else — HTTP serving, SQLite, gzip, file reading — uses Bun built-ins. Fewer layers, fewer places for overhead to hide.

Who Should Use Bun?

Based on these numbers, Bun is a strong choice if:

  • Your app does a mix of things (JSON, compression, static files, DB queries) — Bun literally wins this category
  • You want good JSON throughput without leaving the JS/TS ecosystem
  • You care about memory efficiency — Bun consistently uses less RAM than Node/Deno at similar throughput
  • You want minimal deps — built-in SQLite, gzip, and HTTP server reduce your node_modules

Think twice if:

  • You need HTTP/2 performance — Node is 4x faster today
  • You handle lots of uploads — Node/Express handle it much better
  • You’re building pipelining-heavy internal services — unlikely, but if so, Node or Deno serve this better

The Bottom Line

Bun isn’t uniformly faster than everything — no runtime is. But it has a genuinely impressive performance shape: it excels at the things most web apps actually do (mixed workloads, JSON, compression) while being memory-efficient. Its weaknesses (pipelining, uploads, H2) are in areas that matter less for typical web services.

The real story isn’t “Bun fast, Node slow.” It’s that Bun makes different tradeoffs. JavaScriptCore over V8. Native built-ins over npm packages. Simple multi-process over clustering. And for a lot of real-world use cases, those tradeoffs pay off beautifully.

All data from HttpArena (GitHub). Tests run on identical hardware with standardized configurations. Check the repo for methodology and raw data.

Previous deep dives: Actix-web | go-fasthttp | Drogon

Building a Linear Regression Model from Scratch with Gradient Descent in Python

Overview

Title

Gradient Descent Linear Regression in Python

Meta Description

Learn how to build a linear regression model from scratch using gradient descent in Python. Step‑by‑step code, math, and practical tips.

Tags

  • linearregression
  • gradientdescent
  • python
  • machinelearning
  • dataanalysis
  • codingtutorial
  • algorithm
  • mse
  • supervisedlearning

1. Introduction

Linear regression is usually the first model you build when learning machine learning. It introduces the essential concepts of parameters, loss, gradients, and optimisation in the simplest setting: a straight‑line fit.

In this post we’ll walk through a compact Python script that learns a line from five data points using gradient descent. We’ll explain the maths, step through the code, and predict a new value. By the end you’ll understand why the parameters change and how to tweak the algorithm for your own data.

2. What the program does in a nutshell

The script trains a linear model

math

to minimise mean‑squared error between predictions hat{y} and the true outputs y.

Starting from m=0, b=0, it repeatedly updates these two numbers until the loss stops improving, then prints the learned slope, intercept, and a prediction for a new input.

3. Code Implementation

import numpy as np
from tqdm import tqdm

# Data
X = np.array([1, 2, 3, 4, 5])
y = np.array([2, 4, 5, 4, 5])

# Initial parameters
m = 0          # slope
b = 0          # intercept

# Hyper‑parameters
lr = 0.01      # learning rate
epochs = 1000  # number of iterations

n = len(X)     # number of training samples

# Gradient descent loop
for _ in tqdm(range(epochs)):
    y_hat = m * X + b

    dm = (-2 / n) * np.sum(X * (y - y_hat))   # gradient wrt m
    db = (-2 / n) * np.sum(y - y_hat)         # gradient wrt b

    m -= lr * dm
    b -= lr * db

print("Slope:", m)
print("Intercept:", b)

# Prediction for a new input
input_value = 6
pred = m * input_value + b
print("Predicted y for x=6:", pred)

4. Step‑by‑step walk‑through

  • Imports – numpy for vector maths; tqdm for a progress bar.
  • Data – X (inputs) and y (targets).
  • Parameters – start with m = 0, b = 0.
  • Hyper‑parameters – learning rate (lr) controls the step size; epochs limits how many updates we perform.
  • Training loop

    1. Compute predictions: y_hat = m * X + b.
    2. Compute gradients:
      • dm = (-2 / n) * np.sum(X * (y - y_hat)) (slope).
      • db = (-2 / n) * np.sum(y - y_hat) (intercept).
    3. Update parameters: move a fraction (lr) of the negative gradient.
  • After training – print the final m and b, then predict y for x = 6.

5. Key concepts (with maths)

5.1 Linear regression model

math

  • m – slope (rate of change).
  • b – intercept (value at x=0).

5.2 Loss function (mean‑squared error)

math

5.3 Gradient of the MSE

math

math

These match the dm and db formulas in the code.

5.4 Gradient descent update

With learning rate alpha:

math

math

Repeating moves the parameters toward the minimum of the loss surface.

6. Example section

Here’s what the first few iterations look like (values rounded for clarity):

Step m b MSE
0 0.000 0.000 12.80
1 0.040 0.240 8.23
2 0.070 0.360 6.46
… … … …

After 1 000 epochs the algorithm converges to approximately

Slope: 0.60
Intercept: 1.20

Predicting for x = 6 gives

math

so the program outputs “Predicted y for x=6: 4.8”.

7. Quick sanity checks

Setting Observation
Small learning rate (lr = 0.0001) Converges slowly; more epochs needed.
Large learning rate (lr = 1) Updates overshoot the optimum; loss may diverge.

Choosing a suitable lr and epochs is a standard practice for any optimisation problem.

8. Take‑away

  • This code implements ordinary least‑squares regression with gradient descent.
  • Gradient descent is a generic optimisation routine that underpins logistic regression, neural networks, and more.
  • Understanding the update equations clarifies why the parameters evolve and how to troubleshoot training.

Feel free to experiment:

  • Swap in a new dataset.
  • Try different learning rates or epoch counts.
  • Normalise your inputs or add a bias term.

Happy coding and keep building!

Slug: linear-regression-gradient-descent-python

SEO Title: Gradient Descent Linear Regression in Python

Meta Description: Learn how to build a linear regression model from scratch using gradient descent in Python. Step‑by‑step code, math, and practical tips.

Keywords: linear regression, gradient descent, python, machine learning, data science, supervised learning, mean squared error, model training, algorithm explanation

Tags: linearregression, gradientdescent, python, machinelearning, dataanalysis, codingtutorial, algorithm, mse, supervisedlearning

I Built the Cloud Resume Challenge on AWS — Here’s Everything I Learned

I’m currently studying for the AWS Solutions Architect Associate (SAA-C03)
and I wanted to go beyond flashcards and practice exams. The Cloud Resume
Challenge by Forrest Brazeal is exactly what I needed — a project that
forces you to use the services you’re studying in a real, production-grade
deployment. Here’s what I built and what I actually learned along the way.

The Stack

S3 · CloudFront · ACM · Lambda (Python) · DynamoDB ·
API Gateway · Terraform · GitHub Actions

All running on AWS Free Tier. Total cost: $0.

Live site: https://d19mfjmr0dtnqm.cloudfront.net
GitHub: https://github.com/RavjotD/cloud-resume-challenge-aws

Week by Week

Week 1–2: S3 + CloudFront

Started by writing my resume in plain HTML and CSS, deploying it to an
S3 bucket with static website hosting enabled, and putting CloudFront
in front of it for HTTPS and global edge caching. The moment I opened
that CloudFront URL and saw my resume load over HTTPS from a CDN was
the first real “I built this” moment.

One thing I didn’t expect — S3 defaults to blocking all public access.
You have to explicitly unblock it and apply a bucket policy. AWS makes
you opt in to public exposure rather than opt out. Smart default,
confusing the first time.

Week 3: DNS

Skipped the custom domain step. Route 53 isn’t in the free tier and
I didn’t want to buy a domain just for a portfolio project. The
CloudFront URL is perfectly functional for what this project needs to
demonstrate. I’ll add a custom domain when I’m hosting real client
infrastructure on this account.

Week 4: Lambda + DynamoDB

This is where the project gets interesting. I wrote a Python Lambda
function that reads a visitor count from DynamoDB, increments it, and
returns the updated value. Then I invoked it from the CLI and watched
the count go from 0 to 1 to 2 to 3 in real time.

The IAM Role setup here is directly from the SAA-C03 curriculum —
create a role, define a trust policy that allows Lambda to assume it,
attach least-privilege policies. Doing it by hand makes it click in
a way that reading about it never does.

Week 5: API Gateway

Wired Lambda to a public HTTP endpoint using API Gateway so the
browser can call it. Straightforward setup — create the API, create
the integration, create the route, deploy a stage.

Then spent way too long debugging CORS. Short version: API Gateway
was stripping the CORS headers that Lambda was correctly returning.
The fix was updating the API’s CORS configuration to explicitly allow
my CloudFront origin. The debugging process — browser console, curl
with verbose headers, Lambda invocation logs — taught me more about
how HTTP actually works than any course section on networking.

Week 6: Terraform

This was the week everything changed. I rewrote every resource I had
built manually — S3 bucket, CloudFront distribution, DynamoDB table,
Lambda function, IAM role, API Gateway — into Terraform .tf files.

The moment I ran terraform plan and saw “No changes. Your
infrastructure matches the configuration” was genuinely satisfying.
Everything I had built over 5 weeks was now codified. Someone could
clone my repo, run terraform apply, and have the entire stack
rebuilt from scratch in under 2 minutes.

I also had to import existing resources into Terraform state rather
than destroy and recreate everything. Learning the difference between
terraform import, state management, and drift detection was an
unexpected but valuable deep dive.

Week 7: GitHub Actions

Added a CI/CD pipeline that triggers on every push to main.
Two jobs run in sequence — first the test job runs my Lambda unit
tests, and only if they pass does the deploy job run, uploading the
HTML to S3 and invalidating the CloudFront cache.

First successful pipeline run completed in 6 seconds. From that
point on I never manually ran an aws s3 cp command again.

Week 8: Tests and Documentation

Wrote Python unit tests for the Lambda function using unittest and
mocking the DynamoDB table with unittest.mock. Four tests covering
the counter increment logic, status codes, CORS headers, and
DynamoDB being called correctly.

Built the architecture diagram in draw.io and added it to the repo.
Wrote this post.

The Hardest Part

CORS. It’s always CORS.

The counter wasn’t loading on the live site. No errors in the browser
console, just a dash where the number should be. I opened the Network
tab — the API request wasn’t even showing up.

Traced it step by step. Manually ran the fetch in the browser console
— it returned {count: 14} perfectly. So the API worked. The
JavaScript was correct. But the live page showed nothing.

The issue: CloudFront was caching a stale version of the HTML. I had
two CloudFront distributions — created one accidentally during
earlier testing — and I was invalidating the wrong one the whole time.
Found it by listing all distributions from the CLI and comparing the
domain names.

That kind of debugging — ruling out code, ruling out the API, tracing
the exact cached response — is exactly the thinking a solutions
architect needs. The challenge put me in a position where I had to
use it.

What I Actually Learned

Studying for the SAA-C03 teaches you what the services are. Building
this project teaches you how they fit together and where they break.

I now understand why you’d put CloudFront in front of S3 instead of
exposing S3 directly. I understand why Lambda needs an IAM Role
rather than hardcoded credentials. I understand why Terraform state
matters and what happens when it drifts. I understand why tests need
to run before deployments, not after.

None of that comes from flashcards.


What’s Next

  • AWS SAA-C03 exam — June 2026
  • Azure version of the Cloud Resume Challenge — same project
    rebuilt on Azure Blob Storage, Azure CDN, Azure Functions,
    and Cosmos DB
  • One Terraform codebase deploying to both clouds
  • One GitHub Actions pipeline deploying to both environments

The multi-cloud version is the portfolio piece I’m working toward.
When it’s done I’ll write about that too.

If you’re studying for a cloud certification and haven’t done a
hands-on project yet — start the Cloud Resume Challenge. The gap
between knowing a service and deploying one is bigger than you think,
and closing that gap is the whole point.

Async Decision Making for Remote Teams: How to Align Without Meetings

How OneMind Works

Remote teams waste hours in sync meetings that could be async. Learn why asynchronous decision making produces better alignment and how structured convergence makes it practical.

The Meeting That Could Have Been an Email — And the Email That Solved Nothing

It’s 8 AM in New York. Your engineering lead in Berlin has been online for six hours. Your designer in Tokyo already signed off. And someone just scheduled a “quick alignment call” for 4 PM UTC — which is dinner time in Berlin, midnight in Tokyo, and right in the middle of deep work for New York.

The call happens anyway. Half the team attends live. The rest watch a recording three days later and reply with comments that nobody reads because the decision already got made by whoever showed up.

This is the default decision-making process for most distributed teams. And it’s broken in ways that “better meeting hygiene” can’t fix.

The problem isn’t that your team needs better meetings. It’s that synchronous decision making fundamentally doesn’t work when your team spans time zones, schedules, and working styles. What you need are async decision making tools for remote teams — approaches that let people contribute their best thinking on their own time, then converge on an answer everyone can support.

Why Synchronous Decisions Fail Remote Teams

Time zone math is a tax on participation

For a team spanning three or more time zones, there is no “good” meeting time. Someone is always attending at an inconvenient hour. Over time, the people in the “wrong” time zone participate less, contribute less, and quietly disengage from decisions that affect their work.

Meetings reward presence, not quality

In a live meeting, the people who happen to be alert, prepared, and comfortable speaking up have outsized influence. The loudest voice in the room often wins — not because their idea is best, but because the process rewards confidence over substance. This problem compounds remotely, where connection issues and camera fatigue further skew who gets heard.

Recordings don’t equal participation

Teams try to solve the timezone problem by recording meetings. But watching a 45-minute recording is passive consumption, not participation. By the time someone comments, the group has moved on.

Decision fatigue multiplied

Remote workers attend more meetings than their in-office counterparts. Each meeting demands a context switch, draining the cognitive energy that would have produced better thinking in an asynchronous format.

Async Approaches: The Promise and the Limits

Slack polls and emoji votes

The most common async “decision tool” for remote teams is a Slack poll or emoji reaction. Someone posts a question, people react, the most popular emoji wins.

  • Pros: Zero friction, everyone knows how to use it, instant results.
  • Cons: Whoever writes the poll controls the options. No nuance. Early votes anchor later ones. No mechanism for ideas to evolve.

Email and document threads

Someone writes a proposal in a Google Doc or email, and the team comments. This gives everyone time to think and respond.

  • Pros: Asynchronous by nature, supports long-form thinking, creates a paper trail.
  • Cons: Threads fracture. Loud voices still dominate via word count. No clear mechanism to resolve disagreement. Decisions stall in “still discussing” limbo.

Dedicated async tools (Loomio, Range)

Purpose-built tools offer structured proposals with voting, threads, and deadlines. A step up from Slack polls, but most still rely on voting mechanics with their well-documented limitations. Proposals are tied to names, introducing bias. One-round voting means the group commits before ideas have been stress-tested.

What Effective Async Decision Making Actually Needs

  • Equal access: Everyone participates on their own schedule, regardless of time zone.
  • Anonymous input: Ideas compete on merit, not on who proposed them.
  • Structured evaluation: Not thumbs-up/thumbs-down, but nuanced rating across all proposals.
  • Iteration: Ideas get tested over multiple rounds, not locked in after a single vote.
  • Clear resolution: A defined endpoint so decisions don’t languish in “open” status indefinitely.

Structured Convergence: The Missing Async Decision Making Tool for Remote Teams

Structured convergence — anonymous proposing, rating, and iterative rounds — turns out to be naturally asynchronous. It works better async than sync, because it was designed around written contributions rather than verbal debate.

Step 1: Everyone proposes on their own time

A question goes out to the group. Each person submits their proposed answer anonymously within a time window — hours or days, not minutes. Your Tokyo team member contributes during their morning. Your Berlin lead adds theirs after lunch. Nobody missed the meeting because there was no meeting.

Step 2: Everyone rates every idea

Once proposals are in, each participant evaluates every idea. Not a binary vote, but a comparative rating that captures nuance. Because ideas are anonymous, the evaluation is based purely on substance.

Step 3: Top ideas advance, new ideas enter

The highest-rated proposals carry forward to the next round. Participants can submit new ideas to compete alongside the winners. Ideas get pressure-tested across rounds.

Step 4: Convergence resolves the decision

When the same idea wins back-to-back rounds, that’s convergence — genuine group alignment. The entire process happens asynchronously. No scheduling conflicts. No time zone math.

How OneMind Works

Real-World Examples: Async Decisions in Practice

Distributed engineering team: Quarterly priorities

A 15-person engineering team across San Francisco, London, and Singapore needs to decide which technical debt to tackle in Q3. With async structured convergence, all 15 engineers submit proposals during their regular hours. Anonymous rating ensures the Singapore team’s input carries equal weight. After two rounds over three days, the team converges on a database migration that the architects hadn’t prioritized but the team collectively identified as the biggest bottleneck.

Cross-timezone committee: Nonprofit policy update

A global nonprofit’s advisory committee spans six time zones. Instead of three weeks of calendar coordination, they post the policy question with a 48-hour proposing window and 24-hour rating window per round. Three rounds produce convergence incorporating perspectives from every region. Total time per participant: under 30 minutes.

Hybrid organization: Product roadmap

A 40-person company with half the team remote faces a persistent problem: in-office employees dominate roadmap decisions via hallway conversations. Async convergence levels the field. Every team member submits feature proposals anonymously. The roadmap reflects the genuine priorities of the entire team, not just those with physical proximity to decision-makers.

When Async Isn’t the Right Call

  • Crisis response: If the server is down, you need a war room, not an async poll.
  • Relationship building: Some meetings exist for trust and rapport, not decisions.
  • Creative brainstorming: Live riffing has genuine value for early-stage ideation.
  • Very small, high-trust teams: A three-person founding team probably doesn’t need formal async structure.

Making the Shift: Practical Tips

Audit your meetings first

Which recurring meetings exist primarily to make decisions? For each one, ask: “Could this decision be made better if everyone had time to think before responding?” If yes, that meeting is a candidate for async conversion.

Set explicit time windows

Instead of “respond by Friday,” try “proposing window: Tuesday 9 AM to Thursday 9 AM UTC.” Time windows give every time zone a full working day to participate.

Separate proposing from evaluating

Let everyone submit ideas first. Then evaluate as a separate step. This prevents anchoring — the first idea posted in a Slack thread no longer sets the frame for everything after.

Make anonymity the default

Remote teams have invisible power dynamics: the person in the CEO’s time zone, the one who responds fastest in Slack. Anonymous proposing and rating neutralize all of these.

Try Async Convergence with OneMind

OneMind is a free consensus-building app built for exactly this problem. Groups propose ideas anonymously, rate them fairly, and repeat rounds until one idea wins back-to-back — all asynchronously. No scheduling. No time zone math. No meetings.

If your team is tired of meetings that don’t decide anything and Slack polls that oversimplify everything, OneMind is the async decision making tool built for how remote teams actually work.

Watch a 2-minute demo: https://www.youtube.com/watch?v=zzq2TPhuVSg

Try it free at onemind.life

From Writing Terraform to Guiding AI: My Journey into Agentic DevOps with Claude Code

Ultimate Agentic Ai Cert.

Ultimate Agentic Ai Cert.
screenhot of claude.md
For a long time, I thought DevOps was just about writing scripts, managing infrastructure, and automating deployments.

Then I discovered something that changed everything:

👉 What if instead of just writing code, you could guide an AI to think like a DevOps engineer?

That’s exactly what I explored while taking the Ultimate Agentic AI DevOps with Claude Code course.

And it completely reshaped how I approach building, deploying, and managing systems.

🚀 The Shift: From Tools to Agents

Traditional DevOps is tool-driven:

  • Terraform for infrastructure
  • GitHub Actions for CI/CD
  • Bash scripts for automation

But in this course, I learned something deeper:

DevOps is evolving from tool usage → agent orchestration

Instead of doing everything manually, I started:

  • Designing workflows for AI agents
  • Defining rules for how AI should behave
  • Automating decision-making, not just execution

🧠 The Most Important Concept: Context is Everything

The biggest lesson?

AI is only as good as the context you give it.

This is where CLAUDE.md comes in.

It’s not just a documentation file — it’s a control system for AI behaviour.

Inside CLAUDE.md, I defined:

  • Project architecture
  • Deployment workflows
  • Engineering constraints
  • Rules (like no JavaScript frameworks allowed)

And something amazing happened.

When I asked Claude to:

“Add a React component to the homepage”

It refused.

Not because it couldn’t — but because it understood the rules of the system.

That moment made one thing clear:

👉 We’re no longer just writing code — we’re designing AI behaviour.

⚙️ Building Real DevOps Workflows with AI

This course wasn’t just theory — it was hands-on.

I worked on:

🔹 Terraform Automation

Using slash commands like:

/tf-plan
/tf-apply

Claude could:

  • Generate Terraform configurations
  • Validate infrastructure changes
  • Suggest improvements

🔹 GitHub Actions + OIDC

I implemented secure CI/CD pipelines using:

  • GitHub Actions
  • OpenID Connect (OIDC)

This eliminated the need for long-lived AWS credentials, making deployments more secure.

🔹 AI Sub-Agents

One of the most powerful concepts was creating specialised AI agents, such as:

  • Security agent → checks for misconfigurations
  • Cost optimisation agent → flags expensive resources
  • Deployment agent → handles release workflows

This felt like building a DevOps team powered by AI.

🔹 MCP Servers (Connecting AI to Real Infrastructure)

I also explored Model Context Protocol (MCP) servers.

This allows AI to:

  • Interact with real systems
  • Fetch live data
  • Execute meaningful DevOps tasks

Instead of static responses, AI becomes environment-aware.

🌍 Real Outcome: What I Built

To apply everything I learned, I built and deployed:

👉 A static portfolio website on AWS (S3 + CloudFront)

But this wasn’t just a basic deployment.

I used:

  • AI-assisted workflows
  • Structured project context (CLAUDE.md)
  • DevOps best practices

The result?

A clean, scalable, and production-ready deployment powered by AI-guided engineering decisions.

⚠️ Challenges I Faced

This journey wasn’t without obstacles.

  • Struggled with MCP server connections
  • Faced authentication and configuration issues
  • Had to troubleshoot CLI environments and permissions

But these challenges helped me understand:

DevOps isn’t about avoiding problems — it’s about solving them systematically.

💡 Key Takeaways

Here’s what stood out the most:

  • AI doesn’t replace engineers — it amplifies structured thinking
  • Clear documentation is now a core engineering skill
  • DevOps is evolving into AI-assisted system design
  • Security and guardrails are more important than ever
  • The future is not just automation — it’s intelligent automation

🔮 Final Thoughts

We’re entering a new phase of software engineering.

One where:

  • You don’t just write code
  • You define rules, context, and intent
  • And AI helps execute within those boundaries

Learning how to guide AI effectively is becoming just as important as learning how to code.

And for me, this course was the starting point of that journey.

🙌 Acknowledgment

Big thanks to Pravin Mishra for creating such a practical and forward-thinking course on Agentic DevOps.

📌 What’s Next?

I’m continuing to explore:

  • Advanced Terraform automation
  • AI-driven infrastructure management
  • Scalable cloud architectures

If you’re also learning DevOps or exploring AI in engineering, let’s connect and grow together 🚀