I Spent 6 Months Fixing RAG. Here’s What I Found (And Built)

This is the story of a debugging session that turned into a research paper.

The Bug That Started Everything
I was building a document Q&A system — nothing exotic. Standard RAG setup. FAISS index, SBERT embeddings, GPT as the reader. Classic.

It worked fine on simple questions. “What is the refund policy?” → correct answer.

Then I tested it on a multi-hop question: “What are the environmental compliance requirements for facilities that process the chemicals used in the manufacturing process described in section 4.2?”

The model’s answer was confident. Detailed. And completely wrong.

The retrieved documents were all there. Every piece of information needed to answer correctly was in the context window. But the model still hallucinated a number that appeared nowhere in any document.

I started logging. What I found was two distinct failure modes happening simultaneously:

Failure Mode 1: Semantic Drift. By the time the query had been reformulated for multi-hop retrieval, the embedding had drifted so far from the original intent that we were retrieving the wrong documents. Not slightly wrong. Documents from a completely different section of the corpus.

Failure Mode 2: Context Poisoning. Even when we retrieved mostly correct documents, 1–2 tangentially related but contradictory chunks were slipping through. And those poison chunks were enough to derail the model.

Standard RAG has no defense against either of these. The pipeline is essentially: embed → retrieve → stuff into context → hope.

I needed something better.

Six Months Later: VORTEXRAG
I’m releasing the full framework today. 7 layers, each targeting a specific failure mode. Here’s what I built and why each layer exists.

Layer 1: Tri-Vector Encoding (TVE)
The problem: Single-vector embeddings collapse too much information. “The bank charged a fee” and “The river bank was steep” share a close embedding in SBERT space even though they’re semantically unrelated in most retrieval contexts.

The solution: Three encoding arms:

Semantic arm: standard SBERT 768d

s = sbert_model.encode(chunk) # shape: (768,)

Syntactic arm: POS + dependency structure

t = syntactic_encoder(chunk) # shape: (64,)

Causal arm: verb-argument chains

c = causal_encoder(chunk) # shape: (32,)

Fused vector

v = concat([α·s, β·t, γ·c]) # shape: (864,)
The causal arm is the key innovation. It captures “X causes Y” relationships that pure semantic similarity misses entirely. This is what allows the pipeline to distinguish between a document that mentions a concept and a document that explains the causal mechanism behind it.

Layer 2: Vortex Retrieval Cone (VRC)
The problem: Flat cosine similarity treats all high-similarity documents equally. But document #1 and document #47 in your ranked list shouldn’t have equal weight — there’s a natural falloff in relevance.

The solution: Spiral ranking inspired by vortex dynamics:

spiral_rank = TVE · e^(−λr) · cos(nθ)
Where r is the radial distance (rank position) and θ is an angular phase that encodes causal depth. Documents with high causal relevance get a phase advantage that can overcome a slightly lower semantic similarity score.

In practice: the top-k documents returned by VRC are causally denser than those returned by a flat cosine search on the same index.

Layer 3: Semantic Drift Corrector (SDC)
The problem: Multi-hop queries reformulate themselves at each hop. Each reformulation can drift slightly from the original intent. Over 3–4 hops, this compounds into a completely different query.

The solution: Track the embedding trajectory. At each hop:

drift = query_embedding – anchor_embedding
SDS = 1 – tanh(np.linalg.norm(drift) / τ)

if SDS < 0.72:
query_embedding = re_anchor(query_embedding, anchor_embedding)
The SDS score (Semantic Drift Score) measures how far we’ve drifted. Below 0.72, we re-anchor to the original query intent. This single intervention eliminated most of our multi-hop hallucinations.

Layer 4: Context Poison Guard (CPG)
The problem: 1–2 contradictory or off-topic chunks in the context window is enough to poison the model’s answer. We needed a way to identify and remove these before they reach the LLM.

The solution: Entity-salience ratio per chunk:

ESR = sum(SDS_i * w_i for each entity i) / (num_propositions + ε)

if ESR < 3.5:
flag_for_purging(chunk)
The purging algorithm is provably greedy-optimal (I include the formal proof in the paper). It maximizes total ESR across the retained context while respecting the token budget.

In ablation studies, removing CPG alone dropped faithfulness from 0.94 to 0.75 — an 0.19 point drop from a single layer.

Layer 5: Rank Fusion Gate (RFG)
The problem: Most rank fusion methods are additive. A single terrible signal gets diluted but doesn’t eliminate a bad document.

The solution: Multiplicative fusion:

Φ = TVE^α × SDS^β × ESR^γ
If any of the three signals is near zero, Φ collapses toward zero. A document that scores 0.9 on semantic similarity but 0.1 on CPG gets Φ ≈ 0.09 — effectively eliminated.

This was a deliberate design choice. In high-stakes retrieval (medical, legal, financial), you want a veto mechanism, not a popularity contest.

Layer 6: Causal Context Builder (CCB)
The problem: LLMs have dramatically higher attention to the beginning and end of their context window. Documents buried in the middle get “lost” — the Lost-in-the-Middle problem (Liu et al., 2023).

The solution: Reorder chunks by causal depth:

position = rank * causal_depth

High causal depth → lower position number → placed at context start

Chunks that are causally central to answering the question get placed at the beginning of the context window, where attention weights are highest. Peripheral context gets pushed to the middle where it does less damage if ignored.

Layer 7: Faithfulness Verifier (FV)
The problem: Even with all 6 preceding layers, some hallucinations slip through. We need a final gate.

The solution: Score the candidate answer against the retrieved context:

ΔR = 1 – (ROUGE_L * NLI_entailment_score)

if ΔR > 0.15:
regenerate_answer()
If the answer diverges more than 15% from the source documents (measured by ROUGE-L weighted by NLI entailment), it gets thrown out and regenerated. This catches the subtle hallucinations — cases where the model paraphrases correctly but changes a critical number or name.

Results
Tested on 4 standard benchmarks (NQ, HotpotQA, MuSiQue, 2WikiMultiHopQA):

System EM F1 Faithfulness
Naive RAG 61.2 68.4 0.71
HyDE 63.8 71.2 0.74
Self-RAG 65.4 73.1 0.79
FLARE 64.9 72.8 0.77
VORTEXRAG 74.8 82.6 0.94
+13.6 EM. +14.2 F1. +0.23 Faithfulness over naive RAG.

The ablation shows all 7 layers contribute. The two biggest individual contributors are CPG (+0.19 faithfulness) and SDC (+0.08 EM on multi-hop benchmarks specifically).

Quick Start

pip install vortexrag

from vortexrag import VortexRAG, VortexConfig

config = VortexConfig(
sdc_threshold=0.72,
cpg_esr_threshold=3.5,
fv_delta_r_threshold=0.15,
)

rag = VortexRAG(config)
rag.index(your_documents)

answer = rag.query(“Your complex multi-hop question here”)
What’s Next
The framework is MIT licensed. The full research paper (with formal proofs) is on Zenodo.

If you’re building RAG systems and hitting hallucination walls — especially on multi-hop or domain-specific queries — this framework is designed for exactly that problem.

GitHub: https://github.com/vignesh2027/VORTEXRAG

Paper: https://doi.org/10.5281/zenodo.20285144

Live demo docs: https://vignesh2027.github.io/VORTEXRAG

Questions? Drop them in the comments — happy to go deep on any of the layers.

Post these tonight at 9:00 PM IST. Reddit first (r/MachineLearning, then r/LocalLLaMA 30 minutes apart), then Dev.to. Cross-link the Dev.to article in your Reddit comments with “I wrote a deeper walkthrough here” — that drives traffic both ways.

Day 1: I Finally Understood What Expo Actually Is (And Why It Exists)

What I thought I knew
Honestly, I thought Expo was just “React Native but easier.” I had no idea what it was actually doing under the hood. I just ran npx create-expo-app and hoped it worked.

What I actually learned
Expo is not React Native itself — it’s a toolchain that sits on top of React Native. The best way I can explain it:

React Native = the engine. Expo = the full car with the steering wheel, seats, and GPS already installed.

When you start a React Native project without Expo, you have to configure Android Studio, Xcode, simulators, native dependencies — before writing a single line of code. Expo handles all of that for you.
The 4 key pieces I learned:

  1. app.json — This file is your app’s identity. Your app name, icon, splash screen, permissions, and version number all live here. Expo reads it every time you build.
  2. Metro Bundler — When you run npx expo start, Metro starts up. It takes all your JavaScript files and bundles them into one file the device can run. It also watches for changes and hot-reloads your app instantly — no manual refresh needed.
  3. Expo Go — This is a free app you install on your phone. Instead of building a full APK every time you want to test, your phone connects to Metro over Wi-Fi and runs your code directly. This makes development incredibly fast.
  4. Expo SDK — A collection of pre-built modules for camera, location, notifications, file system, and more. You don’t have to write any native Android or iOS code to use them.

One real example
Here’s what a basic app.json looks like and what each field means:
json{
“expo”: {
“name”: “MyFirstApp”, // what users see
“slug”: “my-first-app”, // URL-safe identifier
“version”: “1.0.0”, // your app version
“icon”: “./assets/icon.png”, // your app icon
“splash”: {
“image”: “./assets/splash.png”
},
“android”: {
“package”: “com.yourname.myfirstapp” // unique ID on Play Store
}
}
}

One thing I still want to explore
The difference between Managed Workflow and Bare Workflow in Expo. From what I understand, Managed = Expo handles the native code, Bare = you get full control. I want to understand exactly when you’d switch from one to the other. That’s tomorrow’s rabbit hole.

Why My Smart Security Camera Was Actually Pretty Dumb (Until I Gave It Memory)

Most retail security setups work like this: you hire someone to stare at a wall of cameras and hope they catch something. The cameras never forget a frame, but the person watching them absolutely does. The entire system depends on a human staying alert for eight hours, scanning sixteen feeds simultaneously, and somehow not missing the one moment that matters.
I built SentinelAI to flip that model. The system watches every camera feed continuously, computes a real-time threat score for every person it detects, fires alerts the instant something looks off — and critically, it remembers. When someone who was flagged last Tuesday walks back into the store on Friday, the system already knows their face.
_

What the System Actually Does

_
At its core, SentinelAI is a FastAPI backend running YOLOv8n inference on a live video feed, a React frontend displaying real-time detection data, and a persistent memory layer powered by Hindsight that ties known faces to historical incidents.
The backend streams annotated video frames via MJPEG to the dashboard. Every ~100ms, a detection loop runs YOLO on the current frame and extracts bounding boxes for every person detected. From there, a suspicion scoring function produces a float in [0, 1] using three weighted components:

def _compute_suspicious_score(
    self, people_count: int, confidences: List[float], frame_area: int, boxes: list
) -> float:
    # component 1: crowd factor (saturates at 5+ people)
    crowd = min(people_count / 5.0, 1.0)

    # component 2: avg YOLO detection confidence
    avg_conf = sum(confidences) / len(confidences) if confidences else 0.0

    # component 3: spatial density — total bbox area vs frame area
    total_box_area = sum((x2 - x1) * (y2 - y1) for x1, y1, x2, y2 in boxes)
    density = min(total_box_area / max(frame_area, 1), 1.0)

    score = 0.4 * crowd + 0.3 * avg_conf + 0.3 * density
    return round(min(score, 1.0), 3)

When score exceeds 0.6, an alert is logged to alerts.json and surfaced on the dashboard. Scores above 0.8 escalate to HIGH severity. The frontend classifies these into four threat bands — NOMINAL, ELEVATED, GUARDED, CRITICAL — each rendered with its own color, animated arc gauge, and severity indicator on the UI.
That part was straightforward. The harder problem was memory.
Integrating Persistent Face Memory
The memory layer works as a lookup that runs in parallel with the YOLO detection pipeline. When a face is detected and a suspicion score computed, the system queries Hindsight with a face embedding vector. If there’s a prior record for that individual — previous flags, timestamps, severity history — that context is pulled into the current detection window.

# After detection, before scoring decision:
face_embedding = extract_face_embedding(frame, box)
memory_context = hindsight_client.recall(face_embedding, top_k=3)

if memory_context:
    prior_flags = memory_context[0]["metadata"]["flag_count"]
    last_severity = memory_context[0]["metadata"]["last_severity"]
    # Boost score for repeat offenders
    if prior_flags >= 2 or last_severity == "HIGH":
        score = min(score + 0.2, 1.0)

This small adjustment has a disproportionate effect on accuracy. A person who is genuinely acting suspiciously tends to score near the threshold repeatedly — not quite 0.8 on any single detection, but consistently above 0.6. With memory, those repeated near-misses compound into a definitive high-severity flag. Without memory, each incident looks ambiguous in isolation.
On the “retain” side, any time a person crosses the alert threshold, the system stores their face embedding and incident metadata back to Hindsight:

alert = {
    "id": len(self.alerts) + 1,
    "timestamp": now,
    "people_count": people_count,
    "suspicious_score": score,
    "severity": "HIGH" if score >= 0.8 else "MEDIUM",
}
hindsight_client.retain(face_embedding, metadata=alert)
self._save_alert(alert)

Every flagged detection becomes part of a growing institutional memory. The system gets better the longer it runs, without any model retraining.
You can explore how Hindsight handles this kind of agent memory at hindsight.vectorize.io — the documentation covers the retain/recall/reflect loop in detail.

The Frontend: What Security Personnel Actually See
The React dashboard renders all of this in a format that makes it immediately actionable. The layout has three panels:
Left sidebar — active camera list with per-camera status (ACTIVE / ALERT / WARN / CRITICAL), person count per zone, and a live AI analysis engine indicator with activity bars showing inference throughput.
Center — the live MJPEG video feed streamed from /video_feed, with bounding boxes overlaid by OpenCV on the backend. Green boxes for normal detections, red for suspicious. This keeps the frontend thin — no client-side inference, just rendering.
Right panel — the threat meter (an animated SVG arc gauge), current threat classification, and a scrollable incident log showing every flagged detection with timestamp, severity badge, score percentage, and person count.
The incident log is the part security staff actually use in practice. Instead of cycling through camera feeds manually, they can scan a chronological list of everything the system flagged in the last shift, then jump to the corresponding timestamp in the camera recording if something warrants investigation.

Architecture at a Glance

Camera Feed (webcam / IP camera / video file)
        │
        ▼
FastAPI Backend (/video_feed endpoint)
  ├── YOLOv8n inference (person detection only, class 0)
  ├── Suspicion score computation
  ├── Face embedding extraction
  ├── Hindsight recall (prior incidents for this face)
  ├── Alert threshold check → Hindsight retain (if flagged)
  └── MJPEG stream to frontend
        │
        ▼
React Frontend
  ├── Live video feed (img src="/video_feed")
  ├── ThreatMeter component (arc gauge + severity bands)
  ├── AlertsSidebar (camera status + incident log)
  └── Real-time polling → /status endpoint (people count, score)

The backend deliberately does no face recognition on its own — that’s Hindsight’s job. YOLO’s role is detection and localization. Face embeddings are handled by a separate lightweight model. This separation of concerns makes it easy to swap out either component without touching the other.

What Surprised Me
_The scoring heuristic is surprisingly robust. _I expected to spend a lot of time tuning the weights (0.4 / 0.3 / 0.3 for crowd, confidence, and density). In practice, the initial values worked well on test footage. Density — the ratio of total bounding box area to frame area — is a particularly useful signal because it catches both tight clustering of people and individuals who position themselves very close to the camera, which is often correlated with concealment behavior.
Memory changed the character of alerts. Before integrating Hindsight, the incident log felt noisy — borderline detections at 0.61 or 0.63 were technically above threshold but often false positives. With the prior-flag boost applied to known repeat detections, the score distribution shifted. Genuine incidents clustered at the top of the range; borderline cases mostly stayed borderline unless reinforced by history. Fewer false positives, same true positive rate.
Streaming over MJPEG is simpler than WebSockets for this use case. I considered WebSockets for the video feed, but a plain targeting a StreamingResponse from FastAPI works fine and has zero client-side complexity. The limitation is that you can’t do bidirectional control (e.g., PTZ camera commands) over MJPEG, but for a read-only monitoring dashboard it’s the right call.

Lessons Worth Taking
Don’t fight the stateless model — add a memory layer. YOLOv8 is excellent at what it does. Trying to make it stateful by hacking pose history or bounding box tracking into the inference loop is the wrong abstraction. Let the detection model detect; let Hindsight remember.

Score thresholds need context. A 0.65 suspicious score means something different for a first-time visitor than for someone with three prior HIGH-severity flags. Memory makes this distinction possible.

The incident log is the product. The live feed is satisfying to watch, but the scrollable log of timestamped, severity-ranked incidents is what actually saves time. Security staff don’t watch feeds — they investigate alerts. Design for that.

Keep the backend doing the heavy lifting. Doing inference on the server and streaming annotated frames keeps the frontend thin. If you need to scale, you scale the backend. The frontend is just a display.
Persistence matters from day one. Alerts logged to alerts.json on day one become training data for refining the scoring heuristic on day thirty. Don’t throw that data away.

The full project source, setup instructions, and dependency list (fastapi, uvicorn, ultralytics, opencv-python) are available in the repo. If you’re building something similar, the Hindsight documentation and the agent memory overview on Vectorize are the best starting points for the memory integration layer.
Retail theft costs the industry billions annually. The technology to build a genuinely intelligent security layer — one that watches everything, forgets nothing, and gets better over time — is already available and not particularly expensive to run. It just needs to be assembled correctly.
_That’s what SentinelAI does.
_

AI isn’t replacing junior devs. Your org chart is.

Two of this year’s most-shared pieces on AI’s effect on junior software engineers read as direct contradictions.

In What about juniors?, Marc Brooker (VP, Distinguished Engineer at AWS) argues that junior developers have a structural advantage. Their accumulated heuristics haven’t calcified into reflex yet, so they don’t have to unlearn anything. The field, he says, is “more powerful than ever” for someone willing to expand their scope.

Around the same time, Mark Russinovich (CTO, Deputy CISO at Microsoft Azure) and Scott Hanselman (VP, Member of Technical Staff at Microsoft) published Redefining the Software Engineering Profession for AI, a paper arguing the exact opposite: AI imposes a “drag” on early-career developers. Juniors must steer, verify, and integrate AI output before they have an opportunity to develop the judgment required to do it well. If organizations focus only on short-term efficiency, they “risk hollowing out the next generation of technical leaders.”

Both papers were widely discussed, both were treated as the definitive take, and they appear to flatly disagree.

They don’t.

What if both sides are right about AI and junior developers?

Brooker is describing what individual junior developers can do. The person who arrives without wrong heuristics, who’s comfortable expanding scope into business context and system ownership, who carries a pager and learns economics alongside code. That person will thrive. The absence of accumulated assumptions becomes an advantage when the assumptions are expiring anyway.

Russinovich and Hanselman are describing what happens to the cohort of junior developers. When companies stop hiring juniors, or hire them and pair them with AI agents instead of experienced engineers, or measure only short-term output velocity, the organizational pipeline dries out, because the structure fundamental to developing judgment has been removed.

Both claims are true, operating at different scales. Brooker is talking about individual disposition. Russinovich and Hanselman are talking about organizational design. The apparent contradiction doesn’t exist, they’re just zooming into different levels.

Which means the question worth asking isn’t “will AI hurt or help junior developers?” but “who is making that decision, and are we making it on purpose?”

How automation has reorganized professions before

Arthur M. Wellington, a 19th century railroad engineer known for his work on infrastructure economics, defined engineering as “the art of doing well with one dollar what any bungler can do with two after a fashion.”

The “art” in his example is the art of building economically. When execution becomes cheap, the profession reorganizes around judgment, trade-offs, and constraint management.

And this is a pattern, not a prediction.

The clearest quantitative example comes from manufacturing. When CNC machines automated metalworking through the 1970s and onward, operators running hand-operated tools were displaced. But the profession didn’t shrink. In Computerized Machine Tools and the Transformation of US Manufacturing, a 2022 NBER study tracking four decades of data, the research team found that employment for college graduates in affected industries rose 86% from a low base, while losses hit high school graduates at 7-8%. The tasks that grew in demand were, in the researchers’ words, “more conceptual and socially connected, and require more training, preparation, and learning” than the ones that declined.

Execution became programmable, and the profession reorganized around judgment, customization, and system-level thinking.

You probably already heard the spreadsheet version of this story. After Excel’s introduction, bookkeepers and clerks shrank from 2 million to 1.5 million, but accountants, auditors, and financial managers surged. When the ledger work got cheap, the profession moved upstream into analysis and advisory.

The most structurally similar case, though, is playing out in a different discipline right now: large law firms are shrinking junior ranks as discovery and contract review are being automated.

The human center shifts to narrative construction, litigation strategy, and client counseling. But moving professions upstream isn’t that easy: Discovery was historically where junior lawyers developed judgment. The grind of reviewing thousands of documents taught pattern recognition, relevance filtering, and adversarial thinking. Automating the execution layer removed the drudgery – along with the training ground.

Sound familiar?

The pattern is consistent: when execution gets cheap, professions reorganize around judgment. The question that varies, case by case, is whether the reorganization includes the next generation, or discards them.

The hiring decisions you’re already making – whether you’re aware or not

Every company that chose not to open a junior headcount this quarter made this decision. Every team that paired a senior engineer with an AI coding agent instead of an early-career developer made it too. Every manager who measures output velocity without accounting for the mentorship that isn’t happening anymore makes it by default.

These are decisions being made today. Mostly unintentionally.

The complication that makes this harder than “just hire juniors and pair them up”: Brooker’s other essay from around the same time, My heuristics are wrong. What now?, describes an “extinction-level event for rules of thumb.” Seniors are simultaneously revising their own heuristics while being asked to transmit judgment. The experienced engineer’s mental model of system maintainability, code costs, API design, and service boundary assumptions is actively being invalidated in near-real time.

This is why Russinovich and Hanselman’s preceptor model (structured mentoring through paired practice) specifies a year-long pairing as equals, not a senior dispensing wisdom to an intern. The model works because both parties are learning together. The senior contributes pattern recognition and system context. The junior contributes comfort with AI tooling and freedom from heuristics that no longer apply. Neither is the teacher. Both are learning.

Your choice, if you’re hiring, is between making this structural decision consciously or letting it happen unintentionally – having a hundred headcount conversations without ever considering your pipeline.

If you’re a junior: How to build judgment without waiting for your org

Everything above is about forces acting on you. Forces you can’t change, like organizational decisions, cohort dynamics, and pipeline economics.

But you can still optimize, regardless of which way your company chooses.

Brooker’s prescription is straightforward: expand scope. Engage with business context, customer needs, economics, and trade-offs. Own systems. Carry a pager. Don’t wait for the structure to develop your judgment. Build it yourself by going where the ambiguity is.

Albert Zhao, a fellow AWS engineer, made this concrete in a recent video: rather than “learn to code better,” the path is “learn to make decisions about systems”: understand why a service boundary exists where it does, why a particular trade-off was chosen, and what the second-order effects are when requirements change.

The organizational design question is one you can’t control. What you can control is whether you’re building the kind of judgment that matters on either side of the decision.

Version-controlled macOS system settings

Houston is a native macOS app for managing hidden preferences through visual profiles you can save as files and commit to your dotfiles.

Today I shipped Houston, a native macOS app for managing 120+ hidden settings through searchable, visual profiles.

The problem

A lot of the macOS settings I care about aren’t in System Settings. Dock auto-hide delay, screenshot save location and format, Finder path bar, press-and-hold accent characters — they’re buried in defaults write commands, scripts, and online documentation such as macos-defaults.com.

What Houston does

Houston gives you toggles for those settings in one SwiftUI app. You can save your Mac’s configuration as a profile, commit it to your dotfiles repo, and apply it to a new Mac in one click.

Features

  • 120+ macOS settings: preferences Apple buried — Dock, Finder, Desktop, Safari, Accessibility, Trackpad, Keyboard, screenshots, and more — exposed as toggles instead of defaults write commands.

  • Save your setup as a profile: capture every tweak in a single file. Apply it to a new Mac, a coworker’s Mac, or your own after a clean install — and get the exact same configuration back in one click.

  • Reproducible across machines: profiles are plain files. Commit them next to your dotfiles, share them with your team, and treat your Mac config like code.

  • No Terminal, no System Settings spelunking: every toggle is searchable and grouped. What used to take an afternoon of Googling now takes a minute.

  • Per-display wallpaper management: set different wallpapers for each connected display.

  • Menu bar quick-access: toggle your favorite tweaks without opening the app.

  • Lightweight native SwiftUI: no background daemons or agents.

  • macOS Tahoe and later: built for the latest system APIs.

Pricing

One-time $4.99. No subscription. You can apply 5 tweaks for free before being required to enter a license key.

Try Houston Today

https://houstonmac.app

Requires macOS 26 Tahoe and Apple Silicon.

🚀 If you commit a Houston profile to your dotfiles, send it my way — I’d love to see how people configure their Macs!

Dr. Shawn Schwartz is a Senior Product Data Scientist at Slack. He completed his PhD in Psychology at Stanford, studying how attention shapes memory, and has been writing software for 15+ years. He writes about scientific computing and bringing research instincts into product work. Subscribe to Sustained Attention.