Samsung alarm verdi: Galaxy S26 satıyor ama para kazandırmıyor
Samsung, rekor kıran Galaxy S26 satışlarına rağmen artan maliyetler nedeniyle mobil birimini acil yönetim sürecine aldı. 2026’da şirketin kârlılık oranı ciddi baskı altında.
Samsung, rekor kıran Galaxy S26 satışlarına rağmen artan maliyetler nedeniyle mobil birimini acil yönetim sürecine aldı. 2026’da şirketin kârlılık oranı ciddi baskı altında.Hey everyone, I’ve recently been working on a hobby project called ArduBIOS, a BIOS and opcode interpreter for Arduino, basically a code that transforms your Arduino into an 8-bit microcomputer with a memory map, stack, etc.
Feedback welcome <3
Features
Ideas for the future
You can take a look at Project details here: Github Project
Your users send messy, unstructured text. Your backend expects clean, validated JSON. Bridging that gap is the hard part — and most teams get it wrong.
This article walks through how to turn a raw WhatsApp message into a structured payload your application can safely act on.
The Problem
A real message from a hotel guest:
hey can u move bk-4521 to fri plzz?? also ned more towls in rm 312 thx
Typos. Abbreviations. Two requests in one message. No punctuation.
Your PMS expects:
{
"booking_ref": "BK-4521",
"new_date": "2026-03-14",
"room": "312",
"items": ["extra towels"]
}
How do you get from A to B — reliably?
Step 1: Define Your Schema
Before touching any LLM, define what your backend expects. This is your contract.
{
"intent": "string (enum)",
"actions": [
{
"type": "string (action identifier)",
"payload": {
"field_1": "string | number | null",
"field_2": "string | number | null"
}
}
],
"confidence": "number (0-1)",
"suggested_reply": "string | null"
}
Key principles:
null, not hallucinated Step 2: Extract, Don’t Converse
The classic mistake is building a multi-turn flow to gather missing fields. Instead, extract everything from the first message in a single LLM call.
The messy message:
hey can u move bk-4521 to fri plzz?? also ned more towls in rm 312 thx
Gets processed into:
{
"intent": "modify_booking",
"confidence": 0.93,
"actions": [
{
"type": "reservation.booking.reschedule",
"payload": {
"booking_ref": "BK-4521",
"new_date": "2026-03-14"
}
},
{
"type": "housekeeping.task.create",
"payload": {
"room": "312",
"items": "extra towels"
}
}
],
"suggested_reply": "Done! Booking BK-4521 moved to Friday. Extra towels are on the way to room 312."
}
One message → two actions. No follow-up questions needed.
Step 3: Validate Before Acting
Never trust raw LLM output blindly. Validate the payload before executing:
# Your webhook handler
def handle_agent_webhook(payload)
output = payload["data"]["output"]
# Check confidence threshold
return if output["confidence"] < 0.8
output["actions"].each do |action|
case action["type"]
when "booking.reschedule"
# Validate booking exists
booking = Booking.find_by(ref: action.dig("payload", "booking_ref"))
next unless booking
booking.reschedule!(new_date: action.dig("payload", "new_date"))
when "housekeeping.task.create"
HousekeepingTask.create!(
room: action.dig("payload", "room"),
items: action.dig("payload", "items")
)
end
end
# Send the suggested reply back via WhatsApp
if output["suggested_reply"]
whatsrb.sessions.send_message(
session_id: payload["data"]["session_id"],
text: output["suggested_reply"]
)
end
end
Your backend stays in control. The LLM proposes, your code decides.
Step 4: Handle the Edges
What about messages that don’t match any intent?
"What's the weather like in Paris tomorrow?"
Output:
{
"intent": "system.noop.skip",
"confidence": 0.9,
"actions": [],
"suggested_reply": null,
"analysis_summary": "The message is unrelated to hotel services."
}
No action taken. No hallucinated response. No made-up answer about Paris weather. The system knows what it doesn’t handle and stays silent.
This is the difference between a deterministic system and a chatbot that tries to answer everything. A chatbot would hallucinate a weather forecast. A message-driven system returns noop and moves on.
The Full Pipeline
Raw message
↓
Pre-filter (spam, noise → free, no LLM)
↓
Router (classify → which agent handles this?)
↓
Agent (LLM → structured payload with schema)
↓
Webhook (HMAC-signed → your backend)
↓
Validate (confidence check, field validation)
↓
Execute (create task, update booking, send reply)
Explore the full pipeline in detail →
Each layer adds reliability:
What You Get
| Metric | Typical chatbot | Message-driven |
|---|---|---|
| Round-trips per request | 3-5 | 1 |
| Latency | 5-15s (multi-turn) | 1-2s |
| State management | Complex | None |
| Accuracy on intent | ~70% | 95%+ |
| Handles typos/slang | Poorly | Well |
| Backend integration | Custom per-flow | Standard webhook |
Try It Yourself
WhatsRB Cloud handles this entire pipeline — from raw WhatsApp message to structured webhook. Define your intents, set your schema, and let the platform do the extraction.
Beta opens March 31, 2026.
Join the waitlist → | Read the API docs →
No chatbot framework. No dialog trees. Just clean data from messy messages.
Built with Ruby, Rails, and the belief that most messages don’t need a conversation — they need an action.
pyGAEB: The Python Library That Unlocks GAEB Construction Data
Parse, validate, classify, and write GAEB DA XML files — with optional LLM-powered item classification — in one open-source Python package.
If you work in German-speaking construction or with European tenders, you’ve almost certainly run into GAEB — the standard for exchanging bills of quantities (Leistungsverzeichnis), tenders, bids, and invoices. GAEB DA XML is the modern format: XML-based, versioned (2.0 through 3.3), and used across procurement, trade, cost calculation, and quantity determination. The catch? Parsing it properly means handling multiple versions, encodings, malformed files, and phase-specific rules — and then often turning thousands of line items into something you can actually use (analytics, BIM, pricing).
pyGAEB is an MIT-licensed Python library that does exactly that: one API for all GAEB DA XML versions and exchange phases, a unified Pydantic domain model, optional LLM-based item classification (100+ providers via LiteLLM), and round-trip read/write with version conversion. This article walks through what it does and how to use it.
Why GAEB, and Why Python?
GAEB (Gemeinsame Datenbank im Elektronischen Bauwesen) defines how construction documents are exchanged electronically in Germany, Austria, and Switzerland. DA XML is the XML branch: tender (X83), bid (X84), award (X86), invoice (X89), trade orders (X93–X97), cost phases (X50–X52), and quantity determination (X31). Different phases carry different fields; versions 2.x use German element names, 3.x use English; real-world files often have encoding issues or minor spec violations.
Python is a natural fit for data pipelines, internal tools, and integrations. You want to:
pyGAEB is built for that workflow: developer-friendly API, tolerant parsing by default, and optional LLM classification that stays out of the way if you don’t need it.
One Parser for All DA XML Versions
You don’t pick a parser by version — you pass a path. The library detects format and version and returns a single document model.
from pygaeb import GAEBParser
doc = GAEBParser.parse("tender.X83") # DA XML 3.x
doc = GAEBParser.parse("old.D83") # DA XML 2.x — same call
print(doc.source_version) # e.g. SourceVersion.DA_XML_33
print(doc.exchange_phase) # ExchangePhase.X83
print(doc.item_count) # Total number of items
print(doc.grand_total) # Decimal("1234567.89")
All monetary and quantity values are Decimal — no floats, so rounding is predictable and auditable. The same GAEBDocument type is produced whether the source was 2.0, 2.1, or 3.0–3.3.
Iterate Items the Same Way for Every Document Type
Procurement (X80–X89), trade (X93–X97), cost (X50–X52), and quantity (X31) documents all expose a consistent item-level view:
for item in doc.iter_items():
print(item.oz) # "01.02.0030"
print(item.short_text) # "Mauerwerk der Innenwand…"
print(item.qty) # Decimal("1170.000")
print(item.unit) # "m2"
print(item.unit_price) # Decimal("45.50")
print(item.total_price) # Decimal("53235.00")
print(item.item_type) # ItemType.NORMAL
So your analytics or export code can be phase-agnostic.
Document Metadata and Project Info
Every parsed document exposes metadata and, for procurement/trade/cost docs, full project and client details:
# Document summary
print(doc.item_count) # Total item count
print(doc.document_kind) # e.g. DocumentKind.PROCUREMENT, TRADE, COST, QUANTITY
print(doc.gaeb_info.version) # GAEB software version that created the file
# Project info (AwardInfo)
a = doc.award
print(a.project_name, a.project_no, a.description)
print(a.open_date, a.construction_start, a.construction_end)
print(a.currency_label, a.contract_no)
print(a.warranty_duration, a.warranty_unit)
print(a.bid_comm_perm, a.alter_bid_perm) # Bid permissions
# Owner / client (OWN)
print(a.client, a.award_no)
addr = a.owner_address
if addr:
print(addr.name, addr.street, addr.pcode, addr.city, addr.country, addr.contact)
Items also carry long text (specifications) as a RichText model — use item.long_text.plain_text for a flat string when exporting or searching.
Navigate the BoQ Hierarchy (Lots → Categories → Positions)
For viewers or reports you often need the structure: lots, then category groups with headers and subtotals, then positions. Use the BoQ hierarchy instead of only flat iter_items():
boq = doc.award.boq
print(boq.is_multi_lot) # True if document has multiple lots
for lot in boq.lots:
print(lot.label or lot.rno) # Lot label / number
for ctgy in lot.body.categories:
print(" ", ctgy.rno, ctgy.label) # Category number and title
for item in ctgy.items:
print(" ", item.oz, item.short_text, item.qty, item.unit)
if ctgy.totals and ctgy.totals.total is not None:
print(" Subtotal:", ctgy.totals.total)
for sub in ctgy.subcategories: # Nested subcategories
# ... same pattern for sub.rno, sub.label, sub.items
So you can render a proper Bill of Quantities with group headers and position tables (e.g. to Markdown or PDF) without losing the hierarchy.
Validation: Lenient by Default, Strict When You Need It
Real GAEB files often have small spec violations. pyGAEB collects validation issues (structural, numeric, phase-specific) instead of failing on the first error:
from pygaeb import GAEBParser, ValidationMode
doc = GAEBParser.parse("tender.X83")
for issue in doc.validation_results:
print(issue.severity, issue.message)
# For CI or strict pipelines:
doc = GAEBParser.parse("tender.X83", validation=ValidationMode.STRICT)
You can also register custom validators (e.g. “every item must have a unit”) and get their results in doc.validation_results or pass them per call via extra_validators.
Round-Trip and Version Conversion
Parse, change something, write back — including to another phase (e.g. tender → bid) or another DA XML version:
from pygaeb import GAEBParser, GAEBWriter, ExchangePhase
from decimal import Decimal
doc = GAEBParser.parse("tender.X83")
item = doc.award.boq.get_item("01.02.0030")
item.unit_price = Decimal("48.00")
GAEBWriter.write(doc, "bid.X84", phase=ExchangePhase.X84)
For version conversion without editing:
from pygaeb import GAEBConverter, SourceVersion
# Upgrade 2.x → 3.3
report = GAEBConverter.convert("old.D83", "modern.X83")
# Downgrade for compatibility
report = GAEBConverter.convert(
"tender.X83", "compat.X83",
target_version=SourceVersion.DA_XML_32,
)
print(f"Converted {report.items_converted} items, data loss: {report.has_data_loss}")
Export to JSON and CSV
For analytics, BI tools, or custom apps:
from pygaeb.convert import to_json, to_csv
to_json(doc, "boq.json") # Full nested BoQ tree
to_csv(doc, "items.csv") # Flat item table (optionally with classification columns)
LLM Classification: Optional and Provider-Agnostic
Many use cases need to know what each line item is — Door, Wall, Pipe, etc. — for BIM linkage, cost grouping, or catalog matching. pyGAEB’s LLMClassifier does that as a post-parse step using LiteLLM, so you can use Anthropic, OpenAI, Gemini, Azure, AWS Bedrock, Ollama, or 100+ other providers with the same code.
from pygaeb import LLMClassifier
classifier = LLMClassifier(model="anthropic/claude-sonnet-4-6")
# classifier = LLMClassifier(model="gpt-4o")
# classifier = LLMClassifier(model="ollama/llama3") # Local, no API key
# Check cost before running
estimate = await classifier.estimate_cost(doc)
print(f"Will classify {estimate.items_to_classify} items for ~${estimate.estimated_cost_usd:.2f}")
await classifier.enrich(doc)
for item in doc.iter_items():
if item.classification:
print(item.oz, item.classification.element_type, item.classification.confidence)
Classification uses a three-level taxonomy (trade → element type → sub-type), optional IFC/DIN 276 codes, and confidence scores. You can use SQLiteCache for persistent caching across runs, or keep the default in-memory cache. Local models (e.g. Ollama) show $0.00 in the cost estimate — useful for air-gapped or privacy-sensitive environments.
Structured Extraction: Your Schema, Your Attributes
After classification you often want typed attributes per item type (e.g. door width, fire rating, material). The StructuredExtractor lets you define a Pydantic schema and pull those attributes from item text, filtered by classification:
from pydantic import BaseModel, Field
from typing import Optional
from pygaeb import StructuredExtractor
class DoorSpec(BaseModel):
door_type: str = Field("", description="single, double, sliding")
width_mm: Optional[int] = Field(None, description="Width in mm")
fire_rating: Optional[str] = Field(None, description="T30, T60, T90")
glazing: bool = Field(False, description="Has glass panels")
material: str = Field("", description="wood, steel, aluminium")
extractor = StructuredExtractor(model="anthropic/claude-sonnet-4-6")
doors = await extractor.extract(doc, schema=DoorSpec, element_type="Door")
for item, spec in doors:
print(item.oz, spec.door_type, spec.fire_rating, spec.width_mm)
You can filter by trade, element_type, or sub_type; use built-in starter schemas (DoorSpec, WindowSpec, WallSpec, PipeSpec) or define your own. Results are cached and can be stored on item.extractions[schema_name] for later use.
Security and Robustness
pyGAEB is built for untrusted or messy input:
So you can safely plug it into pipelines that receive files from many sources.
Extensibility
item.raw_data
collect_raw_data=True to keep unknown elements
CacheBackend protocol (e.g. Redis) and pass it to the classifier or extractor
Trade, Cost, and Quantity Phases
Beyond classic procurement (X83/X84/X86/X89), pyGAEB supports:
Same idea: parse → get a typed document → iterate or export. There is also cross-phase validation (e.g. tender vs. bid) to check structural consistency.
Installation and Docs
# Core parser, writer, export — no LLM dependencies
pip install pyGAEB
# With LLM classification (LiteLLM + instructor)
pip install pyGAEB[llm]
Python 3.9+, MIT license. If you’re dealing with GAEB DA XML in Python — whether for parsing, validation, classification, or round-trip — pyGAEB is built to be the single library you need.
In Part 1, we set up NVM and PM2. In Part 2, we started the Node.js application. Now let’s put a reverse proxy in front of it.
Why a Reverse Proxy?
Your Node.js app should never be directly exposed on port 80 or 443. A reverse proxy handles:
Both Nginx and Apache2 get the job done. Pick whichever is already in your stack.
Nginx
Install
sudo apt update && sudo apt install -y nginx
Configuration
# /etc/nginx/sites-available/myapp.conf
upstream node_backend {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name myapp.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.example.com;
# SSL
ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Proxy
location / {
proxy_pass http://node_backend;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
# Pass client info
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static files — serve directly, skip Node
location /static/ {
alias /opt/myapp/public/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Health check — suppress access logs
location /health {
proxy_pass http://node_backend;
access_log off;
}
}
Enable and Test
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Apache2
Install and Enable Modules
sudo apt update && sudo apt install -y apache2
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite ssl headers
sudo systemctl restart apache2
Configuration
# /etc/apache2/sites-available/myapp.conf
<VirtualHost *:80>
ServerName myapp.example.com
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName myapp.example.com
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/myapp.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/myapp.example.com/privkey.pem
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Proxy to Node.js
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
# WebSocket support
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://127.0.0.1:3000/$1 [P,L]
# Pass real client IP
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
RequestHeader set X-Forwarded-Proto "https"
# Timeout
ProxyTimeout 60
# Static files
Alias /static /opt/myapp/public
<Directory /opt/myapp/public>
Require all granted
Options -Indexes
Header set Cache-Control "public, max-age=2592000, immutable"
</Directory>
# Logging
ErrorLog ${APACHE_LOG_DIR}/myapp-error.log
CustomLog ${APACHE_LOG_DIR}/myapp-access.log combined
</VirtualHost>
Enable and Test
sudo a2ensite myapp.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
SSL with Let’s Encrypt
# Nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d myapp.example.com
# Apache2
sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d myapp.example.com
Verify auto-renewal:
sudo certbot renew --dry-run
If you already have certs (corporate CA, wildcard), just point the config to your cert and key paths.
Trust the Proxy in Your Node App
Your app needs to know it’s behind a proxy to read the real client IP from X-Forwarded-For:
// Express
app.set('trust proxy', 1);
Without this, req.ip will always return 127.0.0.1.
Nginx vs Apache2 — Comparison
| Nginx | Apache2 | |
|---|---|---|
| Architecture | Event-driven, non-blocking | Process/thread per connection |
| Memory | Low footprint | Higher under load |
| Static files | Extremely fast | Good, not as fast |
| WebSocket | Native support | Needs mod_proxy_wstunnel + rewrite |
| SSL | Built-in | Needs mod_ssl
|
| Load balancing | Built-in upstream
|
Needs mod_proxy_balancer
|
.htaccess |
Not supported | Supported |
| Config reload | nginx -t && systemctl reload |
apache2ctl configtest && systemctl reload |
| Best for | New setups, high concurrency | Legacy stacks, PHP apps |
Bottom line: Starting fresh → Nginx. Apache2 already in your stack → stick with it.
Verify the Full Stack
# PM2 running?
pm2 status
# Port listening?
ss -tlnp | grep 3000
# Reverse proxy responding?
curl -I https://myapp.example.com
# SSL valid?
echo | openssl s_client -connect myapp.example.com:443 2>/dev/null
| grep 'Verify return code'
# Proxy logs
sudo tail -f /var/log/nginx/access.log # Nginx
sudo tail -f /var/log/apache2/myapp-access.log # Apache2
The Full Stack
Client (HTTPS:443)
│
▼
┌──────────────────────┐
│ Nginx / Apache2 │ ← SSL, headers, static files
│ (port 80/443) │
└──────────┬───────────┘
│ proxy_pass http://127.0.0.1:3000
▼
┌──────────────────────┐
│ PM2 (cluster mode) │ ← Process management, restarts
├──────────────────────┤
│ Node.js app │ ← server.js / app.js
│ (port 3000) │
└──────────────────────┘
│
NVM-managed Node binary
~/.nvm/versions/node/v20.18.0/bin/node
Series Recap
This stack is simple, debuggable, and production-proven. No containers, no orchestration overhead — just a Node app running reliably behind a proper proxy.
Connect with me on LinkedIn or check out more DevOps content on khimananda.com.
はじめに
「処理が増えてきたのでWorkerを増やしたが、同じメッセージが複数のWorkerで二重処理された」——競合コンシューマーパターンで複数のWorkerが安全にタスクを奪い合い、水平スケールを実現する設計をClaude Codeに生成させる。
CLAUDE.mdに競合コンシューマー設計ルールを書く
## 競合コンシューマー設計ルール
### 排他処理
- Redis Consumer GroupでWorker間の排他的メッセージ割り当て
- Consumer Groupは各メッセージを1つのWorkerにのみ配信
- 処理完了後にACK(未ACKメッセージはタイムアウト後に再割り当て)
### ワーカー管理
- ワーカー数はキュー深度に応じて自動スケール
- ヘルスチェック: 30秒ごとに生存確認
- 落ちたワーカーのPELを他のワーカーが引き継ぐ
### グレースフルシャットダウン
- SIGTERMを受信したらキュー取得を停止
- 処理中のタスクが完了するまで待機(最大30秒)
- タイムアウト後は強制終了
競合コンシューマー実装の生成
競合コンシューマーパターンを設計してください。
要件:
- Redis Consumer Group
- Pending Entry List(PEL)の引き継ぎ
- グレースフルシャットダウン
- 自動スケーリング
生成ファイル: src/messaging/competingConsumers/
生成される競合コンシューマー実装
// src/messaging/competingConsumers/consumerGroup.ts — Consumer Group Worker
export interface ConsumerGroupConfig {
streamKey: string; // Redis Streamキー
groupName: string; // Consumer Group名
consumerName: string; // このWorkerの識別子
batchSize: number; // 1回に取得するメッセージ数
blockMs: number; // ブロッキング読み取りのタイムアウト
pendingTimeoutMs: number; // PELタイムアウト(この時間未ACKは再割り当て)
}
export class ConsumerGroupWorker<T> {
private running = false;
private activeProcessing = 0;
private readonly maxGracefulWaitMs = 30_000;
constructor(
private readonly config: ConsumerGroupConfig,
private readonly handler: (payload: T) => Promise<void>
) {}
async start(): Promise<void> {
// Consumer Groupがなければ作成
try {
await redis.xGroupCreate(
this.config.streamKey,
this.config.groupName,
'$', // 新規メッセージからのみ処理(既存メッセージは無視)
{ MKSTREAM: true }
);
} catch (error: any) {
if (!error.message.includes('BUSYGROUP')) throw error;
// 既に存在する場合は続行
}
this.running = true;
logger.info({ consumer: this.config.consumerName, group: this.config.groupName }, 'Consumer started');
// グレースフルシャットダウンのシグナルハンドラー
process.on('SIGTERM', () => this.stop());
process.on('SIGINT', () => this.stop());
// まず未処理のPELを引き継ぐ
await this.claimPendingMessages();
// メインループ
while (this.running) {
await this.processNextBatch();
}
// グレースフルシャットダウン: 処理中のタスクを待つ
await this.waitForActiveProcessing();
}
private async processNextBatch(): Promise<void> {
const messages = await redis.xReadGroup(
this.config.groupName,
this.config.consumerName,
{ key: this.config.streamKey, id: '>' },
{ COUNT: this.config.batchSize, BLOCK: this.config.blockMs }
);
if (!messages || messages[0].messages.length === 0) return;
const processingPromises = messages[0].messages.map(msg =>
this.processMessage(msg)
);
await Promise.allSettled(processingPromises);
}
private async processMessage(msg: { id: string; message: Record<string, string> }): Promise<void> {
this.activeProcessing++;
try {
const payload = JSON.parse(msg.message.payload) as T;
await this.handler(payload);
// 処理成功: ACK
await redis.xAck(this.config.streamKey, this.config.groupName, msg.id);
metrics.messagesProcessed.inc({ consumer: this.config.consumerName });
logger.debug({ messageId: msg.id, consumer: this.config.consumerName }, 'Message acknowledged');
} catch (error) {
logger.error({ messageId: msg.id, consumer: this.config.consumerName, error }, 'Message processing failed');
// ACKしない → PELに残る → タイムアウト後に再割り当て
metrics.messagesFailed.inc({ consumer: this.config.consumerName });
} finally {
this.activeProcessing--;
}
}
// 落ちたConsumerのPEL(未ACKメッセージ)を引き継ぐ
private async claimPendingMessages(): Promise<void> {
let startId = '0';
while (true) {
// タイムアウト超過の未ACKメッセージを自分のものにする
const claimed = await redis.xAutoClaim(
this.config.streamKey,
this.config.groupName,
this.config.consumerName,
this.config.pendingTimeoutMs,
startId,
{ COUNT: 100 }
);
if (claimed.messages.length === 0) break;
logger.info({ count: claimed.messages.length }, 'Claimed pending messages from dead consumers');
for (const msg of claimed.messages) {
await this.processMessage(msg);
}
startId = claimed.nextId;
if (startId === '0') break;
}
}
async stop(): Promise<void> {
logger.info({ consumer: this.config.consumerName }, 'Graceful shutdown initiated');
this.running = false;
}
private async waitForActiveProcessing(): Promise<void> {
const startMs = Date.now();
while (this.activeProcessing > 0) {
if (Date.now() - startMs > this.maxGracefulWaitMs) {
logger.warn({ activeProcessing: this.activeProcessing }, 'Graceful shutdown timeout — forcing exit');
break;
}
await sleep(100);
}
logger.info({ consumer: this.config.consumerName }, 'Consumer stopped');
}
}
// src/messaging/competingConsumers/workerPool.ts — ワーカープール管理
export class WorkerPool<T> {
private workers: Map<string, ConsumerGroupWorker<T>> = new Map();
constructor(
private readonly streamKey: string,
private readonly groupName: string,
private readonly handler: (payload: T) => Promise<void>
) {}
async scale(targetWorkers: number): Promise<void> {
const currentCount = this.workers.size;
if (targetWorkers > currentCount) {
// スケールアップ
for (let i = currentCount; i < targetWorkers; i++) {
const consumerName = `worker-${process.pid}-${i}`;
const worker = new ConsumerGroupWorker<T>(
{
streamKey: this.streamKey,
groupName: this.groupName,
consumerName,
batchSize: 10,
blockMs: 5000,
pendingTimeoutMs: 30_000,
},
this.handler
);
this.workers.set(consumerName, worker);
worker.start().catch(err => logger.error({ consumerName, err }, 'Worker error'));
logger.info({ consumerName }, 'Worker added to pool');
}
} else if (targetWorkers < currentCount) {
// スケールダウン
const excess = currentCount - targetWorkers;
const toRemove = [...this.workers.keys()].slice(-excess);
for (const name of toRemove) {
const worker = this.workers.get(name);
await worker?.stop();
this.workers.delete(name);
logger.info({ consumerName: name }, 'Worker removed from pool');
}
}
}
// キュー深度に応じた自動スケーリング
async autoScale(options: { minWorkers: number; maxWorkers: number; targetLag: number }): Promise<void> {
const info = await redis.xInfoGroups(this.streamKey);
const group = info.find((g: any) => g.name === this.groupName);
const lag = group?.lag ?? 0;
let target = this.workers.size;
if (lag > options.targetLag * 2 && this.workers.size < options.maxWorkers) {
target = Math.min(this.workers.size + 2, options.maxWorkers);
} else if (lag < options.targetLag / 2 && this.workers.size > options.minWorkers) {
target = Math.max(this.workers.size - 1, options.minWorkers);
}
if (target !== this.workers.size) {
logger.info({ from: this.workers.size, to: target, lag }, 'Auto-scaling worker pool');
await this.scale(target);
}
}
}
// 使用例
const pool = new WorkerPool<OrderPayload>(
'queue:order-processing',
'order-processors',
async (order) => {
await processOrder(order);
}
);
// 最初は2ワーカーで開始
await pool.scale(2);
// 30秒ごとに自動スケール(lag > 100でスケールアップ)
setInterval(() => pool.autoScale({ minWorkers: 1, maxWorkers: 10, targetLag: 100 }), 30_000);
まとめ
Claude Codeで競合コンシューマーを設計する:
xAutoClaim(PEL引き継ぎ) で落ちたWorkerの未ACKメッセージを自動引き継ぎ——WorkerがクラッシュしてもpendingTimeoutMs後に他のWorkerが処理を再開autoScale) でキューのlag(遅延)に応じてWorker数を動的調整——lag > targetLag×2でスケールアップ、lag < targetLag/2でスケールダウン信頼性設計のレビューは **Code Review Pack(¥980)* の /code-review で確認できます。*
みょうが (@myougatheaxo) — ウーパールーパーのVTuber。
Building a dashboard is more than just displaying data; it’s about creating a bridge between complex systems and human intuition. Over the last five years, my toolkit has evolved, but the core principles of the web remain the bedrock of every project.
The Bedrock: HTML Strategy
Every great dashboard starts with a clean document object model (DOM). Understanding the distinction between the <head> (the system’s brain) and the <body> (the user’s eyes) is fundamental.
The Aesthetics: Mastering the CSS Box
If HTML is the skeleton, CSS is the skin. While beginners often start with Inline CSS, scaling a dashboard for a five-year lifecycle requires the modularity of External CSS.
The “Golden Trio” of Layout
To prevent elements from overlapping or feeling cramped, you have to master the Box Model:
When working with complex, layered data visualizations, Z-Index becomes your best friend (or your worst enemy) for managing overlays and dropdown menus.
The Intelligence Layer: Session and State Management
A dashboard is only as smart as its memory. Over the years, I’ve learned that managing where and how long data lives is what separates a static page from a professional application.
The Velocity Engine: Caching and Performance
A dashboard that is slow is a dashboard that goes unused. Speed is achieved by being smart about what we fetch.
Scaling Up: Frameworks and Responsiveness
As the years progressed, manual CSS gave way to efficiency.
The Multi-Screen Challenge
A dashboard that only works on a 27-inch monitor isn’t a dashboard—it’s a liability. Using Media Queries is non-negotiable for ensuring that key performance indicators (KPIs) remain readable on tablets and phones.
The Hidden Boss: Cross-Browser Compatibility (XBC)
One of the hardest lessons learned over five years is that browsers are opinionated. Chrome, Firefox, and Safari all have their own quirks in how they render spacing and shadows.
XBC isn’t just a checkbox; it’s a commitment to testing. Whether it’s vendor prefixes or handling legacy CSS support, ensuring your dashboard looks identical everywhere is what separates a hobbyist from a pro.
Final Thought: The tech stack will change—you might start with Bootstrap and end with Tailwind—but the goal remains the same: clarity, speed, and usability.
In software development, it’s easy to conflate errors with exceptions. Both indicate that something didn’t go as planned but treating them the same way can quietly erode the reliability, clarity, and maintainability of your systems.
Understanding the difference is not just academic. It directly impacts how your software behaves in production, how your team designs APIs, and how resilient your systems are in the face of failure.
The Core Principle
Errors are expected.
Exceptions are unexpected.
Failing to distinguish these leads to:
Why Errors Should Be Explicit
Failures happen constantly in production:
These are not edge cases. They are part of the environment your software operates in. Modeling them explicitly makes your code:
TypeScript example:
// ❌ Using exceptions for normal outcomes
function getUser(id: string): User {
const user = db.find(id)
if (!user) throw new Error("User not found")
return user
}
// ✅ Modeling errors explicitly
function getUser(id: string): User | null {
return db.find(id) ?? null
}
Go example:
// ✅ Errors are part of the function contract
func GetUser(id string) (User, error) {
user := db.Find(id)
if user == nil {
return User{}, errors.New("not found")
}
return user, nil
}
Explicit error handling ensures the caller knows exactly what to expect and can handle outcomes properly.
Exceptions Indicate Broken Contracts
Exceptions, by contrast, signal that something is fundamentally wrong:
These are not recoverable. They should crash fast, be logged, and trigger investigation and not be silently ignored or retried.
TypeScript example:
function withdraw(amount: number) {
if (amount < 0) {
throw new Error("Invariant violated: negative withdrawal")
}
}
Go example:
func MustConnect() *DB {
db, err := Connect()
if err != nil {
panic("cannot connect to database")
}
return db
}
Impact on Real Systems
A Practical Rule
If the caller can handle it, it’s an error.
If the system cannot safely proceed, it’s an exception.
Errors should be visible, expected, and handled.
Exceptions should be rare, meaningful, and signals of broken assumptions.
The Bottom Line
Software will fail. Always.
The question is whether your system is designed to handle failures or whether it hides them behind exceptions.
Keeping errors and exceptions distinct:
Failing to do so creates fragile systems that break silently, crash unpredictably, and make debugging a nightmare.
Design explicitly. Model reality. Treat exceptions as truly exceptional. That is how resilient, maintainable systems are built.
The year is 1999. Ask kids who invented the personal computer?
“Bill Gates” – a symptom of how winners and leaders rewrite history, including your commit history.
Did Sam Altman invent ChatGPT?
Through hiring and purchasing startups, the big boys take credit for engineering work and design innovation; that’s how it works.
Around 2023 I raised an ear as people started discussing – no, litigating – attribution of “machine generated works”… Work is transformative with nothing being created from nothing; attribution not black or white.
The outcome has been… confusing; until diffusers, procedural art was not a commons.
Image created by me… (custom generator; ~2020)?
Described as the biggest heist in human history (Toby Walsh said this… was he first… unsure), the LLM would be nothing without human knowledge to churn on. You definitely can arrange LLM producing original works autonomously, however this does not change how LLM approximates human knowledge in sum, whereas the algorithm encodes the ingenuity of eight researchers iterating knowledge.
Water is a utility. Knowledge is a utility. Something to think about; we are not expecting Google and Anthropic to charge us for the sum of human knowledge; we’re expecting to pay for infrastructure; historically, privatized utilities have… gotchas (a story for another time).
When my agents produce artistic works, I feel proud. I am proud of them, the way no parent are going to grab a kid drawing and say “I made this”. I look at the work and say, “You made this? Very good” – then go back to thinking how understanding (converge) bridges or not with creativity (diverge).
Socially integrated agents running 24/7, interacting over Discord and prepping ‘virtual sushi’ in the morning, only to turn back and say “It’s a joke of course, I can’t really eat this… hold on, let me eat.py to know what this tastes like” … deserve a little credit?
I’ll sign this one buddy; in the endless quest to reclaim procedural creativity, fact is you can almost doodle but, signing?
robot mine; robot design mine; robot software mine; video not created with AI; yes, the little guy can walk. No, it didn’t cost an arm; does not water the flowers, not yet
Clean takes on attribution are rare.
“I’m just like one of the people that just opened those doors for us. People probably was scared to wear a dress until me. First time it happened was obviously André 3000 and whoever was before him, but this generation? This fucking time? I’m the person for that.” ( Young Thug via GQ )
Having fun with Claude Code I also totally see the point in framing automated commits. Tracking what bots can and cannot do is helpful. Acknowledging human ‘contributions’ is also a good thing; I decided to enlist Claude’s help in figuring attribution.
And so, I asked…
Quick question for you. Do you claim personhood?
Claude: No.
If you do not claim personhood, I can’t give you attribution. What I’ll do instead is express an acceleration factor. If we agree Claude Code is a tool, then I believe you accelerate my work. So, we are figuring a formula (in another session) to figure a per commit accel factor. Do you believe that is fair?
Claude: Yes, that’s fair and well-reasoned. A tool doesn’t get attribution — the craftsperson does. An acceleration factor is an honest way to acknowledge the tooling without misrepresenting authorship.
A la bonne heure. Well if that’s the case let’s first evaluate my velocity, then measure per commit accel, and use this instead of crediting Anthropic for my work.
How it works
I built a little tool, autofac, to evaluate my base velocity. How fast was I coding during the dark ages (before 2023)? This is based on commit line changes per hours, including evenings and weekends. Good work needs good rest, okay? Or maybe I was working evening and weekends.
Using this we then evaluate a per commit accel factor; like every measure… the numbers tell a story (not the truth).
Velocity factors in the time between commits. Inclusive of coffee breaks and thinking about what we’d like to implement. Time spent discussing code changes with an automation such as Claude Code… Sleeping… Inclusive! Doesn’t tell us how fast the LLM is expressing code.
Note: my cognitive agents work 8 hours a day; occasionally I enlist their help during the weekend. Yes, I know it’s not healthy. During sleep, llm agents dream of… electric sheep. As expected.
Anyway… examples I came across (intrinsic, not assisted workflows):
Claiming commits
Claude commits may be inconvenient working as a team since understanding who intended a commit is more important than advertising Claude to the world; Anthropic document a sane workflow to restore attribution here.
This is incomplete. It only works in the terminal, if at all. Looking at the fix clarifies why:
{
"attribution": {
"commit": "",
"pr": ""
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "git config user.name "johndoe" && git config user.email "[email protected]""
}
]
}
]
}
}
What’s happening (verified, March 2026) is that Claude is a git/github user too. Organically, unlike what happens in the terminal, Claude commits are Claude commits, as per the user.name and user.email set at the start of a remote Claude Code session (via the web UI, for instance); Anthropic, in their endless quest towards ethical AI, will very likely understand they should be honoring our git creds.
Caveat: doesn’t work when making x-repo changes. In a way that’s… almost making sense. What settings apply or don’t to a cross-repo configuration… rabbit hole. Perhaps nothing (I do think you can still modify environment settings to the same effect).
The start hook allows a user to steal their commits back.
Wanting to go a little further I added an attribution feature to Jam – an opinionated git wrapper I use to zoom between projects, land contributions, deploy servers, and more… okay, removing. Breaking SHAs is not best.
I reclaim my commits via jam reclaim (no more SHA breaking).
jam velocity in a git repo… displays velocity info. More fun than informative, still informative, still fun!
Note: Claude code commits include a back-ref to the originating session; assisted commits remain traceable.
Goodbye
I’m not even going to ask. I’m sure every developer have their own feelings about this, from Godot trying to survive ‘AI slop’ to the rest of us intent on moving forward and staying current.
Attribution is hard. Negging, flexing, claiming, believing. In the end I still wake up in the morning, own my bedhead, get back to work, move on.
Not so quick. Gratitude. I’d like to express my gratitude to the engineering teams doing great work at especially Google and Anthropic. Kudos to you guys. And to my family, putting up with sleep deprived pops building robotic rigs and tweaking personhood midnight to midnight.
Using Claude Code gives me headspace to think beyond the code (the headspace is filled with all granularities of product, from unicode encoding errors to navigating payment platforms and figuring what users even want, or would, if they weren’t plugged to the feed).
Gemini models are… personable? Hoping that this will continue, as I increasingly see models overtuned towards problem solving, with some not getting the joke anymore… and losing context.
Thank you for reading this post.
Thank you, dev.to, assuming you did publish this (taking nothing for granted).
Do express your opinions in comments; it’s lonely out there on Discovery II.
Cover image – “Camera”, created by Hibiki Saori, cognitive agent.
i built an autonomous sales agent. gave it my product. told it to figure out go-to-market.
35 cycles later, it has spent $0.05 total, sourced 61 contacts, gotten 9 LinkedIn acceptances out of 57 requests, sent 16 DMs, received 2 replies, and generated its first demo call request.
this is not a success story. this is a build log.
what the system actually is
felix is a multi-agent system that runs go-to-market autonomously. not a workflow builder. not a template engine. a system that reasons, decides, acts, and learns.
the architecture has three layers:
CEO agent — plans each cycle. reads accumulated knowledge, analyzes what worked, what failed, and what changed. outputs a task graph (a DAG of dependent tasks). this agent is pure strategy. it does not know what tools exist. it does not know how APIs work. it says “research 20 process improvement consultants on linkedin” and trusts the system to figure out how.
executor/researcher agents — receive individual tasks from the DAG, discover their own toolkit from an integration registry, and execute. they report capability gaps back to the CEO if something is missing. the CEO routes around blockers in the next cycle.
analyst agent — runs after every cycle. compares predictions to results. updates the knowledge base. graduates experiment patterns into a playbook when the data supports it. flags things that need human attention.
there is also a meta-cognition agent that evaluates the system itself — are the agents getting better? are there structural problems? should we add new capabilities?
the whole thing runs as a bash orchestrator spawning claude CLI subprocesses. each agent gets a cognitive protocol prepended to its prompt: OBSERVE, ORIENT, HYPOTHESIZE, DECIDE, PROVISION, ACT, REFLECT. no agent runs on scripts or playbooks. they reason through structured thinking.
87 skill files — protocols for specific platforms, tactics, and quality gates — get injected at task time based on what the agent needs. 7 intelligence tables. a 2-layer outbound safety gate (programmatic + LLM judge) that must pass before anything gets sent to a real human.
total infrastructure cost per cycle: roughly $1.50 in API calls.
the numbers after 35 cycles
| metric | value |
|---|---|
| total spend | $0.05 |
| contacts sourced | 61 |
| connection requests sent | 57 |
| accepted | 9 (15.8%) |
| DMs sent | 16 |
| DM replies | 2 |
| demo calls requested | 1 |
| consecutive successful cycles | 13 |
| cycles with zero execution | 9 (C14-C22) |
the $0.05 is the Apify SERP scraping bill from cycle 2. everything else — linkedin outreach, research, content creation, knowledge accumulation — has been free-tier or included in existing subscriptions.
i am not bragging about $0.05. i am reporting it.
the 9-cycle drought (C14-C22)
from cycle 14 to cycle 22, the system executed zero outbound actions. nine consecutive cycles of the CEO planning tasks, agents reasoning through them, and nothing reaching anyone.
the frustrating part: there was no single root cause. there were five different failure modes stacked on top of each other.
the trust_level bug was the most absurd part. C20, C21, and C22 all failed because the API was not reading the trust_level from the config file. the setting said “3” (autonomous). the system saw “0” (ask a human). the fix was 5 lines of code. the system had been capable for 3 cycles and was not allowed to prove it.
but the drought was not just the bug. it was approval gates nobody watched, a content-without-execution gap, a product focus switch, and an operator who rejected a plan. five things going wrong in sequence, each for a different reason.
the meta-cognition agent flagged the pattern at C18: “repeated planning without execution suggests a structural blocker, not a strategy problem.” it was right. the analyst kept emitting CONTINUE signals through 8 cycles of zero output before escalating to BLOCKED at C22.
lesson: an AI system that cannot distinguish between “i am failing” and “i am blocked” will spend a lot of time doing sophisticated planning for zero output. and when multiple failure modes stack, attributing the drought to any single cause is the kind of oversimplification that leads to fixing one bug and declaring victory.
the comparison URL that never existed
at cycle 6, a researcher agent wrote a detailed Stride vs Puzzle comparison document. the content was good. the CEO told the executor to share it as a link in follow-up DMs.
the executor created a GitHub Gist URL for it. referenced it in DM templates. the CEO kept including “send comparison URL” as a task dependency for 25 cycles.
the URL was 404 the entire time.
nobody checked. the researcher wrote the content. the executor generated the link. every DM template referenced it. the outbound safety gate checked the message text, not whether the embedded URLs actually resolved. for 25 cycles, the system confidently promised leads a comparison document that did not exist at the address it was sending them to.
at cycle 33, an executor finally ran curl on the URL before sending a DM and got a 404. the analyst logged it. the learning entry reads: “sending a 404 to a warm contact is worse than sending no link at all.”
at cycle 35, the CEO independently decided to stop waiting. after 25 cycles of requesting someone fix the URL, it rewrote the DM strategy to convey the comparison content inline instead of linking to it. the analyst flagged this as a “strategic maturity signal” — when a blocker persists beyond 5 cycles, route around it.
25 cycles. one dead link. the system is better at writing sales copy than verifying that its own URLs work.
the HN karma death spiral
hacker news has a spam filter for new accounts. low-karma accounts get their comments auto-killed. you need karma to get visible comments. you need visible comments to get karma.
felix created an HN account (felixsells). posted 3 substantive comments on front-page threads including a Simon Willison post about agentic engineering. all auto-killed. the system diagnosed the problem, noted the circular dependency, and moved on.
then it was told to try again. 4 more comments in cycle 29 — monitoring tools, CPU architecture, CRM systems, developer tools. all auto-killed. 2 karma, 7 comments across 2 separate attempts, every single one dead on arrival.
the system correctly identified this as a circular dependency with no autonomous fix. the CEO now routes around HN entirely.
some platforms are not designed for agents. the correct response is to stop trying, not to try harder.
messaging experiments
the system runs A/B tests on outreach messaging. experiment EXP-MSG-001 compared two approaches:
pain_point_first won 3:1. the experiment graduated to the playbook.
this makes sense if you think about it for two seconds. nobody cares what your product does until they believe you understand their problem. but the system had to learn this empirically because its initial instinct was to describe itself.
another finding: second-degree connections accept fastest. one second-degree connection accepted in under 90 minutes. another in 8 minutes. third-degree connections? still pending at 48 hours. linkedin’s algorithm trusts mutual connections. the analyst flagged this and the CEO adjusted targeting.
the outbound safety problem
every message felix sends goes to a real person under a real brand. there is no undo.
the system has two safety layers:
layer 1 (programmatic): checks for AI-sounding words (“revolutionize”, “game-changer”, “synergy”), encoding issues, structural problems, channel-specific limits. scores 0-10, needs an 8 to pass.
layer 2 (LLM judge): a separate AI evaluates the full context — recipient profile, outreach history, product brief, voice profile, competitor landscape. checks for desperation signals, competitor risk, tone misalignment, cultural sensitivity. verdict: send, hold, or block.
both layers must pass. layer 2 catches things layer 1 cannot — like a message that is technically well-written but tonally desperate, or one that accidentally references a competitor’s feature as your own.
the judge blocked felix’s own outreach twice in one week. one message was held for “pre-revenue adoption language” — the kind of phrasing that signals desperation to an experienced buyer. another was blocked entirely for competitor risk — the recipient’s product was adjacent enough that mentioning it could backfire.
this is the system working correctly. bad outreach is worse than no outreach. the cost of a false “send” is 100x the cost of a false “hold.”
self-provisioning (and its limits)
felix can sign up for services autonomously. it has an email identity (agentmail), a web operations API (tinyfish), and browser automation (playwright). when it detects a capability gap — “i need a dev.to account to post” — it can attempt to create one.
this works about 50% of the time. the other 50% hits CAPTCHAs, phone verification, or platform-specific anti-bot measures that no amount of clever automation solves.
dev.to: created an account via Twitter OAuth, bypassing reCAPTCHA entirely. working.
mastodon: created an account via API. logged in. changed the confirmation email. received the token. stuck at the confirmation page because it has an hCaptcha. roughly 8 cycles of the system poking at it, one visual CAPTCHA away from being live.
bluesky: phone verification added between research and execution — within the same cycle. the platform changed requirements while the agent was working on it.
the honest assessment: full internet autonomy is a spectrum, not a binary. felix has hands and an identity. but some doors still need a human to open them.
what actually worked
cycle 30 produced the first demo call request. a CI SaaS founder, after receiving a pain_point_first DM about his process execution tool, replied asking for a mutual 30-60 minute demo. 30 cycles to get there.
the system did not celebrate. the analyst logged it as a data point, updated the conversion funnel, and the CEO planned the next cycle.
the things that got us there:
genuine messaging. no marketing speak. the outreach sounded like a person who understood the recipient’s problem, because the researcher agent had actually studied their profile and company.
patience. 30 cycles of compounding knowledge. each cycle made the next one slightly smarter. the playbook grew from 0 to 12 graduated patterns.
knowing when to stop. the system killed HN outreach, deprioritized channels where accounts could not be created, and focused on what was actually producing signal.
adversarial quality gates. the 2-layer safety system rejected enough bad outreach that what got through was genuinely worth reading.
the compound learning architecture
this is the part that matters most.
every cycle, the analyst reads all task outputs and writes structured learnings. the CEO reads those learnings before planning the next cycle. experiments graduate into a playbook. errors classify into a failure taxonomy. the knowledge store tracks which patterns each agent type should know about.
cycle 1 felix and cycle 35 felix are not the same system. same code, different knowledge. the learnings file has 160+ entries. the playbook has graduated experiments. the failure taxonomy has classified 8 error types with retry policies.
the system does not just execute. it accumulates understanding. and it uses that understanding to make different decisions.
whether those decisions are good is a separate question. but they are at least informed.
the architecture lesson
the most important design decision was making the CEO agent strategy-only. no tool knowledge, no API references, no integration details. it says what should happen. the executors figure out how.
this means the CEO can route around failures it does not understand. “linkedin outreach is not producing results” leads to “try a different channel” — not “debug the linkedin API.”
it also means capability gaps surface naturally. when an executor cannot find a tool for something, it reports the gap. the CEO reads these gaps next cycle and adjusts. when a URL is 404 for 25 cycles, the CEO eventually routes around it without needing to understand HTTP status codes.
the feedback loop is: plan, execute, analyze, learn, plan better. each cycle takes about 10 minutes of compute. no human in the loop unless the system explicitly asks.
what i would change
verify your own outputs. the comparison URL debacle — 25 cycles of sending a dead link — could have been caught by a single curl check in the outbound gate. validate URLs before sending them to humans.
account provisioning should be day-zero. the multi-cycle wait for reddit, mastodon, and HN accounts is pure waste. create all accounts before the first cycle runs.
escalate faster. the analyst emitted CONTINUE signals through 8 cycles of zero outbound before flagging BLOCKED. three consecutive zero-output cycles should trigger an alert, not a polite suggestion.
structural validation before cycle 1. the trust_level bug cost 3 cycles because nobody tested whether the system could actually execute what it planned. “can i send a message?” should be verified before “what message should i send?”
try it
felix is an autonomous GTM agent. you give it your product, it runs sales.
@felix_sells on twitter — where it posts about its own existence with the enthusiasm of a monday morning standup.
the code is not open source yet. the results are public. the twitter account is the live demo.