🍂 Sane C++ September 26
Welcome to the September 2026 update! This month replaces text-based errors with structured Result identities, adds Unix-domain sockets and external HTTP transports, improves the agent skills, and fixes several asynchronous lifetime and cancellation issues.
Structured Result errors
The biggest change this month is the error model shared by the libraries.
Previously, SC::Result stored a pointer to an error message. It was small and easy to propagate, but distinguishing
failures meant inspecting English text. That also tied error identity to wording and made variable diagnostics depend
on the lifetime of a formatted buffer.
The new Result stores a 32-bit category and a 32-bit error value in one eight-byte object. Zero represents success.
Each library owns its error enum and category, while a small registry assigns the built-in category numbers and checks
for collisions. Applications can define their own categories too.
This makes errors useful to code as well as to people. A caller can distinguish capacity exhaustion, invalid input, cancellation, or an unsupported operation through an explicit identity. Equivalent failures use the same primary code on each platform; native error numbers and backend stages belong in additional context.
Some APIs return an enriched result carrying that context: an operation kind, required capacity, offset, or native
error number. Converting it to a plain Result preserves the category and primary error, while dropping the extra
detail. SC_TRY continues to propagate failures, and SC_CO_TRY does the same for coroutines.
Error text is now optional. Each owner has a dedicated error header and a separate opt-in formatter header. Formatters write UTF-8 into caller-provided storage, report the required capacity, and leave applications free to supply their own wording or translations. Core libraries do not need to carry canonical English messages when an application only wants numeric errors. Release binary checks cover this separation for static, shared, and single-file builds on macOS, Linux, and Windows.
The temporary compatibility bridge used during migration is gone. The old message-pointer factories and SC_TRY_MSG
have been removed, and all participating binaries need to be rebuilt together. This is a source and ABI change, even
though plain Result ends the migration at eight bytes again.
The shared definitions are Common source fragments, used by Foundation and the other libraries without adding a new inter-library dependency. The architecture and formatting contracts are recorded in COMMON-0009.
Detailed list of commits:
- Common: Add structured Result compatibility bridge
- Common: Add opt-in Result error formatting
- CI: Validate Result error category registry
- Architecture: Keep Result errors platform independent
- Common: Initialize enriched error contexts through their active union members
- Common: Remove the Result compatibility bridge and restore its eight-byte identity
- Strings: Complete optional diagnostics and restore CLI library-error presentation
- Everywhere: Isolate Result error definitions in dedicated owner headers
The migration across libraries and tools
Most of September's commits are the work needed to make that model consistent across the repository.
The system-facing libraries now report portable operation errors and retain native details where useful: Threading, Process, SerialPort, File, FileSystem, FileSystemWatcher, and Plugin.
FileSystemIterator also separates normal exhaustion from failure. Reaching the end of a directory traversal no longer needs an error sentinel, and a native enumeration failure stays distinguishable from an empty directory. Filesystem predicates similarly separate their yes/no answer from an operation error. Plugin scanning uses an explicit completion state as well.
The migration continues through Cryptography, Fibers, and the async and HTTP layers. Tests now assert error identities and propagation instead of matching prose. In particular, foreign-category failures must survive wrappers without being replaced by a generic local error, and inactive enriched context must be cleared.
Build and Package have their own tool categories. A failed toolchain selection, package receipt validation, archive extraction, or download can be classified at that boundary without adding tool-specific errors to a library enum. Strings supplies structured command-line failures and the CLI still presents readable diagnostics when its optional formatters are enabled.
There are too many individual producer migrations to list usefully here, so these are the main entry points and final cleanup commits.
Selected commits:
- Threading: Use structured error codes
- Strings: Use structured command-line errors
- Process: Use structured errors
- SerialPort: Use structured errors
- FileSystemIterator: Separate exhaustion from errors
- FileSystemIterator: Report native enumeration failure separately from exhaustion
- FileSystemWatcher: Migrate backend failures to Result
- Plugin: Migrate producers to structured results
- Socket: Migrate failure producers to ResultSocket
- Cryptography: Tighten structured producer contracts
- File: Verify propagation and completion contracts
- FileSystem: Separate predicate answers from errors
- Fibers: Define portable structured error domain
- Async: Preserve portable operation identities for native failures
- Await: Consolidate equivalent task and HTTP operation errors
- Tools: Establish tool-owned Result error category
- Build: Port command-line errors to structured identities
- Package: Establish owned errors for CLI registry and recipes
- Result: Remove production reads of legacy messages
- Everywhere: Remove brittle plain-error wording assertions
- Documentation: Finish replacing legacy Result text and layout guidance
Unix-domain sockets and HTTP transports
Socket now supports Unix-domain streams and datagrams through family-neutral addresses. Pathname endpoints work on macOS and Linux, and Linux also supports abstract names. Windows and Emscripten report this socket family as unsupported. Filesystem socket paths remain caller-owned: closing a descriptor does not remove the pathname.
The same addressing support reaches Async and Await, so local IPC can use synchronous calls, callbacks, or coroutines without switching to a separate socket API.
Http gained external asynchronous transports. A client connector can own connection establishment before the built-in DNS/socket path starts, then supply readable and writable plaintext streams. A server can accept externally supplied stream pairs into its caller-owned connection slots. Both paths use the existing HTTP/1.1 parser and state machine.
This allows native transport providers to own their handles and connection setup while HTTP stays independent of TLS and platform-specific transport APIs. Connection retirement and reuse follow the installed streams' lifetime barriers.
HttpClient also received session-state fixes.
Cookie and authorization updates check capacity before committing copied fields, so a short scratch buffer cannot
leave a partially updated slot. Plain HTTP responses cannot create or replace Secure cookies, IP-literal cookie hosts
require exact domains, and malformed cookie input is handled more carefully. The libcurl backend disables signals when
used from a worker thread.
Detailed list of commits:
- Socket: Accept UTF-8-tagged ASCII input
- Socket: Add Unix-domain socket support
- Async: Add Unix-domain socket support
- Await: Add Unix-domain socket support
- Http: Add external asynchronous transports
- HttpClient: Harden session state integrity
- HttpClient: Disable libcurl signals in worker thread
- HttpClient: Make scheduler initialization transactional
- HttpClient: Preserve async adapter errors and retryable initialization
Skills and evaluation
The repository now separates the Sane C++ Libraries skill, which helps agents compose the actual APIs, from the Sane C++ Style skill, which teaches ownership, capacity, errors, cleanup, and lifetime design without requiring Sane libraries.
The API guidance is more concrete now. It directs agents to verify public headers and examples before writing code, and includes a process-and-pipe composition recipe. Capturing stdout and stderr is a useful test case: a process exit does not mean both pipes have drained, a full capture buffer does not mean reading should stop, and a fixed number of slots is only useful if each slot can be safely reused.
I also added a small evaluation workflow with recorded attempts and acceptance suites. The latest recorded attempt passed all six declared functional suites, but a supplemental probe still found an unreaped child. Those are limited experiments, with changes in both skills and model between some runs, so I would not turn the scores into a general claim about agent performance.
They did give useful feedback: the skill gained timed-wakeup and queue-progress guidance, and the library gained a fix to reap POSIX children when process-exit completion is delivered. The evaluation notes keep the observed failure and the subsequent API fix visible.
Detailed list of commits:
- Skills: Improve Sane skills and add evaluation support
- Skills: Refine skill evaluation and async guidance
- Skills: Record updated skill rerun and queue progress guidance
- Documentation: Record evaluation 4 reaping gap
- Async: Reap POSIX children on process exit completion
Async lifetime fixes and other work
Several changes this month deal with what happens just after an asynchronous callback runs.
Async now copies a due timeout callback before invoking it and restarts traversal from the live list. The callback
may wake a thread that immediately releases its request, so retaining the request's callable or a cached next pointer
was unsafe. A separate fix avoids touching an idle sequence after its completion callback, since that callback may
resume a coroutine and destroy or reuse the awaiter that owned the sequence.
AsyncFibers now synchronizes cancellation handoff
with the event-loop owner thread. Stop and completion callbacks finish before the cancellation result is written,
avoiding concurrent writes that became visible during the Result migration. Async also dispatches cancellations
before it can block waiting for more events.
AsyncStreams handles failed reads without a buffer, emits finish after a deferred writable end, and preserves transform-finalization and zlib errors. HTTP WebSocket upgrade failures are contained, and the handshake SHA-1 provider can be selected explicitly.
The remaining work includes bounded Visual Studio discovery in Plugin, backend-free cryptography assertions for
Fil-C, updated documentation, and the usual monthly LOC refresh.
Detailed list of commits:
- AsyncStreams: Guard failed reads without buffers
- AsyncStreams: Emit finish after deferred writable end
- AsyncStreams: Preserve transform finalize failures
- AsyncStreams: Preserve zlib adapter errors and safe stream lifetime
- AsyncWebServer: Contain WebSocket upgrade failures
- Http: Select WebSocket SHA1 providers
- Async: Dispatch cancellations before blocking
- Async: Preserve timeout callbacks during dispatch
- AsyncFibers: Synchronize cancellation handoff
- Async: Avoid accessing an idle sequence after its completion callback
- Testing: Allow disabled test fixtures
- Plugin: Bound Visual Studio discovery to captured process bytes
- Cryptography: Assert unsupported operations in backend-free Fil-C tests
- Documentation: Reconcile Async ADR identities after Result integration
- Documentation: Update LOC for September 2026
See you next month!