Mini ama tank gibi: FOSSiBOT F116 Pro

FOSSiBOT F116 Pro akıllı telefon modelinde 4 inçlik ekran, Dimensity 7300 yonga seti, OIS destekli arka kamera, 3700mAh batarya ve 33W hızlı şarj gibi özellikler yer alıyor.

ArduBIOS: Opcode Interpreter and ISA for Arduino

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

  • Basic output system on Serial Monitor (25% of a BIOS)
  • 128 bytes reserved for the stack
  • 512 bytes reserved for user RAM (RAM for programs)
  • Registers and flags (50% complete): (Registers A, B, C, and D and zero, carry, and negative flags)
  • Basic opcodes
  • Jumps and conditional jumps (jmp n; jz n)
  • Simple mathematical operators (addition and subtraction)
  • Stop values ​​and nulls (0x00; 0xFF)

Ideas for the future

  • Complete BIOS (input and output)
  • SD card support as a floppy disk
  • Generic keyboard input support
  • 16×2 monitor support
  • File system as FAT12
  • Public assembler made in NodeJS
  • Interrupt system

You can take a look at Project details here: Github Project

Designing Deterministic Outputs From Unstructured Messages

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:

  • Finite set of intents — your agent only handles what you define
  • Typed fields — each field has an expected type
  • Nullable — missing data is null, not hallucinated
  • Confidence score — so your backend can decide when to auto-act vs. ask for confirmation

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.

Agent noop

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:

  • Pre-filter removes noise before LLM cost
  • Router ensures the right agent handles each message type
  • Schema constrains LLM output to valid shapes
  • Confidence lets you set auto-action thresholds
  • Webhooks decouple processing from action

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

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:

  • Ingest GAEB files from different software (iTWO, ARRIBA, etc.) without caring about version.
  • Validate structure and business rules (e.g. totals vs. qty × unit price).
  • Enrich items with semantic types (Door, Wall, Pipe) for BIM, costing, or grouping.
  • Export to JSON/CSV or write back GAEB for downstream systems.

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:

  • XXE prevention and Billion Laughs protection in XML parsing
  • File size limit (configurable, default 100 MB)
  • Recursion depth limits on hierarchy walks
  • Encoding repair (e.g. mojibake in German text) via ftfy before parsing
  • Malformed XML recovery (two-pass parse with warnings) when possible

So you can safely plug it into pipelines that receive files from many sources.

Extensibility

  • Custom validators — project-specific rules, global or per parse
  • Post-parse hooks — extract vendor-specific XML into item.raw_data
  • Raw data collection — collect_raw_data=True to keep unknown elements
  • Custom taxonomy and prompts for the LLM classifier
  • Custom cache backends — implement the 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:

  • Trade (X93–X97) — orders, order items, supplier info
  • Cost & calculation (X50–X52) — elemental costing, cost elements
  • Quantity determination (X31) — take-off, REB 23.003, catalogs, attachments

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.

Reverse Proxy for Node.js — Nginx and Apache2 Side by Side

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:

  • SSL/TLS termination — Node doesn’t deal with certificates
  • Security headers — added at the proxy layer
  • Static file serving — offload from Node
  • Request buffering — protects Node from slow clients
  • Centralized access logs

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

  • Part 1 — NVM, PM2, startup scripts, log rotation
  • Part 2 — Running the app, cluster mode, memory limits, monitoring
  • Part 3 — Reverse proxy (Nginx + Apache2), SSL, security headers, verification

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.

Claude Codeで競合コンシューマーを設計する:水平スケール・排他処理・ワーカープール管理

はじめに

「処理が増えてきたので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で競合コンシューマーを設計する:

  1. CLAUDE.md にRedis Consumer GroupでWorker間排他割り当て・ACK後にPELから削除・タイムアウト超過で他Workerが引き継ぎ・SIGTERMで処理中タスク完了まで待機を明記
  2. xAutoClaim(PEL引き継ぎ) で落ちたWorkerの未ACKメッセージを自動引き継ぎ——WorkerがクラッシュしてもpendingTimeoutMs後に他のWorkerが処理を再開
  3. グレースフルシャットダウン でSIGTERM受信後に新規取得を停止し、処理中タスクが完了するまで最大30秒待機——Kubernetes Podの入れ替え時にメッセージロストなし
  4. 自動スケーリング(autoScale) でキューのlag(遅延)に応じてWorker数を動的調整——lag > targetLag×2でスケールアップ、lag < targetLag/2でスケールダウン

信頼性設計のレビューは **Code Review Pack(¥980)* の /code-review で確認できます。*

prompt-works.jp

みょうが (@myougatheaxo) — ウーパールーパーのVTuber。

Frontend Development Basics

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.

  • Structure over Style: Mastering block-level elements ensures that your layout doesn’t collapse when data gets heavy.
  • Interactivity: Using form elements and hyperlinks effectively turns a static report into a functional tool.

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:

  1. Padding: The breathing room inside the element.
  2. Border: The boundary.
  3. Margin: The distance between components.

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.

  • Web Browser Sessions: This is the “short-term memory.” A session ends the moment a user closes the tab in the browser. It’s ideal for temporary states that shouldn’t persist once the user leaves.
  • Per-User Server Info: For security and consistency, we maintain user information for the current session on the server side. This ensures the “source of truth” remains protected.
  • Cookies: These act as persistent identifiers. Stored as client-side data, cookies hold per-user info or persistent settings that need to survive a page refresh or a browser restart.

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.

  • The Style Application Cache: We use the cache for assets that are shared between users, such as stylesheets and icons.
  • Non-Primary Storage: It is important to remember that cached data is not stored as primary data. It is a temporary copy designed for speed, not for the permanent storage of records.
  • Automatic Validation: Modern browsers use the cache for automatic validation. This ensures that the system checks for updates behind the scenes, delivering the fastest possible experience without serving stale content.

Scaling Up: Frameworks and Responsiveness

As the years progressed, manual CSS gave way to efficiency.

  • Utility-First: Tools like Tailwind CSS and DaisyUI allow for rapid prototyping without leaving the HTML file.
  • Component Libraries: Material UI (MUI) provides that polished, professional “enterprise” feel out of the box.
  • Customization: Sites like Uiverse.io remind us that even functional dashboards can have a bit of creative flair.

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.

Error Is Not an Exception: Why This Difference Matters in Real Systems

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.

  • Error: A legitimate outcome of a function or operation. Part of normal system behavior.
  • Exception: A violation of assumptions, something the system cannot recover from safely.

Failing to distinguish these leads to:

  • Hidden control flow
  • Noisy logs
  • Fragile APIs
  • Slower debugging and longer incidents

Why Errors Should Be Explicit

Failures happen constantly in production:

  • Users enter invalid data
  • Resources are missing
  • Networks fail

These are not edge cases. They are part of the environment your software operates in. Modeling them explicitly makes your code:

  • Predictable
  • Readable
  • Easy to maintain

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:

  • Invariants are violated
  • Internal state is corrupted
  • Program assumptions are broken

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

  1. Reliability:
    Treating errors as exceptions makes your system break. Requests fail unnecessarily, retries are skipped, and fallback mechanisms are never reached.
  2. Observability:
    If normal outcomes throw exceptions, logs become noisy and alerts meaningless. Real failures are buried under “false positives.”
  3. API Design:
    Functions that throw for expected outcomes create misleading contracts. Callers cannot reason about behavior without reading implementation.
  4. Distributed Systems:
    Failures are guaranteed in distributed environments, timeouts, partial failures, and network partitions. Treating these as exceptions leads to cascading failures and unpredictable behavior.
  5. Team Productivity:
    Confusing errors and exceptions increases cognitive load. Debugging takes longer, and new engineers struggle to understand system behavior.

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:

  • Improves system resilience
  • Clarifies control flow
  • Reduces operational noise
  • Strengthens team understanding of system behavior

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 Attribution Story

Dark tower (image created by me... I think? custom generator)

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.

Dark tower (image created by me... I think? custom generator)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):

  • Full blaze on Unity projects: 50 loc/per hour
  • Challenging work with conceptual overhead while nerding over clean code: 10 loc/hour
  • Hobby mode IoT project in C++: 0.1 loc/hour

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.