Skip to content

deps: bump modernc.org/sqlite from 1.56.0 to 1.57.0 - #11

Open
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/go_modules/modernc.org/sqlite-1.57.0
Open

deps: bump modernc.org/sqlite from 1.56.0 to 1.57.0#11
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/go_modules/modernc.org/sqlite-1.57.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 24, 2026

Copy link
Copy Markdown
Contributor

Bumps modernc.org/sqlite from 1.56.0 to 1.57.0.

Changelog

Sourced from modernc.org/sqlite's changelog.

Changelog

  • 2026-08-19 v1.57.0:

    • Add an opt-in _defensive DSN query parameter that turns on SQLite's defensive mode for the connection, disabling the SQL-level features that let ordinary statements deliberately corrupt the database file. When _defensive=1 (or any strconv.ParseBool true value) is supplied, the driver calls sqlite3_db_config with SQLITE_DBCONFIG_DEFENSIVE immediately after sqlite3_open_v2 and before every other parameter is applied, so the PRAGMAs the driver itself runs, the _pragma list, and every statement the caller prepares are all subject to it. On such a connection PRAGMA writable_schema=ON, PRAGMA journal_mode=OFF and PRAGMA schema_version=N become silent no-ops, and writes to a virtual table's shadow tables (fts5's _data, _idx and so on) and to sqlite_dbpage fail with "table ... may not be modified"; reading those tables, ordinary use of the virtual tables that own them, and VACUUM are unaffected. The flag has no PRAGMA equivalent, so sqlite3_db_config — and therefore a DSN parameter — is the only way to reach it short of dropping to modernc.org/sqlite/lib. The value is parsed before sqlite3_open_v2, so an invalid one fails the connection without creating the database file, and the parameter must appear at most once: a repeated _defensive is an error rather than letting the first value silently win. Absence of the parameter, or _defensive=0, leaves SQLite's default behavior unchanged; existing DSNs continue to work byte-for-byte. Two limits are worth stating plainly, since the name invites more confidence than the flag earns. Defensive mode is a hardening measure, not a sandbox for hostile database files: it is one of several steps SQLite recommends for that purpose, and this build compiles with neither SQLITE_TRUSTED_SCHEMA=0 nor SQLITE_DQS=0 and exposes no authorizer. And it is a property of the connection, not of the database file — a second handle opened on the same file without the parameter is unrestricted.
    • Reject the one DSN combination defensive mode would otherwise swallow in silence. _defensive=1 together with _journal_mode=OFF (or _journal=OFF) now fails the connection instead of opening one in which neither parameter was honoured: SQLite turns PRAGMA journal_mode=OFF into a no-op that still reports success, so the driver would have accepted the mode, executed it, and left the journal untouched without telling anyone. The check runs in the validation phase introduced in v1.55.0, before any statement executes, so a rejected DSN cannot leave the database half-configured. _pragma remains the exception it has always been: _pragma=journal_mode(OFF) alongside _defensive=1 still runs and is still silently ignored by SQLite. Only DSNs using _defensive can be affected, and that parameter is new, so no DSN that opened before changes behavior.
    • See [GitHub pull request #6](modernc-org/sqlite#6), thanks wsman!
    • Ship the sqlite-vec license notice this module has been missing. modernc.org/sqlite/vec has bundled the transpiled sqlite-vec sources since v1.47.0, but the module carried only its own BSD-3-Clause LICENSE and the public-domain SQLite notice. sqlite-vec is Copyright (c) 2024 Alex Garcia, dual-licensed Apache-2.0 OR MIT and used here under MIT, whose terms require the copyright and permission notice to accompany substantial portions of the software — which 2.8 MB of transpiled vec/ plainly is. The notice now ships as LICENSE-SQLITE_VEC in the module root, byte-identical to the LICENSE-MIT in the upstream v0.1.9 archive and named after the file modernc.org/libsqlite_vec extracts it into. Attribution was never absent — vec's package documentation has named the extension, pinned the version and linked upstream — but the license text itself was, and the omission was ours: vendor_libs/main.go copied the per-target transpiles and nothing else. It now copies the notice alongside them and fails the vendoring run if it cannot, so a make vendor can no longer quietly drop it. The vec package documentation gained a License section recording that the package is under a different license from the rest of this module.
    • The SQLite notice is renamed from SQLITE-LICENSE to LICENSE-SQLITE; update any direct links to it. Its contents are unchanged and SQLite remains public domain. The name now matches both the new LICENSE-SQLITE_VEC beside it and the LICENSE-<upstream> convention every other modernc.org repository follows, but it is more than cosmetic: go mod vendor selects the files it copies into a downstream vendor/ tree by matching each name against a fixed list of prefixes — LICENSE among them — so a name merely ending in LICENSE was never propagated. Both bundled notices now travel with the code into vendored builds, which is where the MIT terms on vec/ keep applying. No code changes; no behavior changes.
    • Let a caller-constructed Driver register its own functions, collations and virtual table modules. Driver has always held four categories of registration state, but only RegisterConnectionHook could put anything on a constructed one: functions and collations were reachable through the package-level API alone, and modules through the package-level driver only, which left the modules field written and read through that instance and so process-global state wearing a per-instance field. Driver now has RegisterFunction, RegisterScalarFunction, RegisterDeterministicScalarFunction, RegisterCollationUtf8 and RegisterModule, plus Must* variants of the first four, each registering on that Driver alone; the methods are safe to call concurrently, and the zero Driver is ready to use as-is. vtab.RegisterModule also honours its db argument now: a non-nil db registers on the driver backing it when that driver implements the new vtab.ModuleRegisterer, while a nil db keeps targeting the driver this package registers as sqlite. One existing pattern changes behavior, narrowly and loudly: vtab.RegisterModule(db, ...) where db was opened on a caller-constructed Driver used to discard the db argument and land on the sqlite driver, reaching every connection in the process; it now lands on the constructed driver alone, so a sql.Open("sqlite") connection that used to resolve such a module gets no such module instead. The same pattern is also the one way an existing program could hold one module name on both a constructed Driver and the package-level one: there the first of the two registrations used to win and the second was refused as already registered, whereas now the package-level implementation wins on the constructed Driver's connections regardless of the order they ran in. Reaching that case at all means the program ignored an error the older version returned. Two smaller deviations round out the list: Driver.RegisterModule reports no error for such a collision, and vtab.RegisterModule now validates its name and module arguments before the not-implemented check, so a call with an empty name that returned vtab: RegisterModule not wired into engine outside this driver returns vtab: module name must be non-empty instead. Everything else is additive against v1.56.0: the package-level registration functions target the same driver they always did, connections still receive every module registered through the package-level path whichever Driver opened them, and a db opened on the sqlite driver resolves to that same driver. The isolating change discussed in [GitLab issue #254](https://gitlab.com/cznic/sqlite/-/issues/254) is deliberately not made here.
    • See [GitLab merge request #135](https://gitlab.com/cznic/sqlite/-/merge_requests/135), thanks Ian Chechin!
    • Promote freebsd/386, freebsd/arm and netbsd/amd64 to fully supported platforms. All three are now listed in the package documentation's platform table, which had carried seventeen entries while this module shipped, cross-built and tested twenty. They arrived as experimental in v1.53.0 — netbsd/amd64 reviving a port that had been broken for years, freebsd/386 replacing a stale, effectively untested SQLite 3.41 transpile, and freebsd/arm entirely new — and were deliberately kept out of that table until they had accumulated real-world exposure, with promotion promised once "a period of broader real-world testing … elapses without surprises". That period has elapsed: all three have been in the builder test matrix and in make build_all_targets since v1.53.0, all three pass the full test suite on this release's commit alongside the seventeen platforms already listed, and no open issue reports a defect in any of them. The two netbsd/amd64 build failures filed before the revival, [GitLab issue #202](https://gitlab.com/cznic/sqlite/-/issues/202) and [GitLab issue #234](https://gitlab.com/cznic/sqlite/-/issues/234), no longer reproduce at this commit: Xsqlite3_is_interrupted is present in the sources that target selects, and the mu.enter/mu.leave symbols that broke the build are gone. Documentation only — the transpiled sources under lib/ are byte-for-byte what v1.56.0 shipped, and nothing about how these targets behave changes.
  • 2026-08-03 v1.56.0:

    • Re-vendor the transpiled SQLite sources, picking up modernc.org/libsqlite3's fix for an upstream data-corruption bug in SQLite 3.53.3's journal rollback. The SQLite version is unchanged at 3.53.3; what changes is that the amalgamation is now patched before it is transpiled. 3.53.3 reworked readSuperJournal() to return the super-journal name through a char** out-parameter, and pager_playback() now tests that pointer where it used to test zSuper[0]. A crash during the commit of a multi-database (ATTACH) transaction can leave the super-journal name and its checksum zeroed while the name length and the trailing magic survive; the checksum is a plain byte sum, so an all-zero name still validates and readSuperJournal() hands back a non-NULL pointer to an empty string. pager_playback() then calls sqlite3OsAccess(pVfs, "", SQLITE_ACCESS_EXISTS), gets ENOENT, and deletes the hot journal without playing it back — leaving the database corrupted. This is not a transpilation artifact: a plain gcc build of the stock 3.53.3 amalgamation fails on the same bytes while 3.53.2 recovers them, and it is what has been making upstream's own test/crash.test fail intermittently, in roughly 2% of runs, on every platform. The patch restores the pre-3.53.3 behaviour of reporting a (nul) super-journal name and will be dropped once upstream ships its own fix. Every supported target carries it.
    • Two targets change beyond that patch. On linux/s390x the regenerated transpile allocates C bit-fields MSB-first, as the big-endian platform ABI requires, rather than LSB-first; this comes from modernc.org/cc/v4 v4.29.1 and touches bit-field accesses throughout the SQLite core, s390x being this module's only big-endian target. On linux/riscv64 the transpile was regenerated on a host running GCC 11.4.0 where the previous one used GCC 13.3.0, which drops a handful of unexported compiler-predefined macro constants (the __FLT16_* family, __DBL_IS_IEC_60559__ and friends) and changes the COMPILER=gcc-13.3.0 entry PRAGMA compile_options reports to COMPILER=gcc-11.4.0; no SQLite code generation differs. Every other target's generated code is byte-identical to v1.55.0 apart from the journal-rollback patch above.
    • Bump the pinned modernc.org/libc to v1.74.4, and the remaining dependencies to their current releases. v1.74.2 and v1.74.3 are retracted upstream — a freeaddrinfo lock leak that deadlocks name resolution — and v1.74.4 is the fix. As always, downstream modules must pin the exact modernc.org/libc version this module's go.mod pins (see [GitLab issue #177](https://gitlab.com/cznic/sqlite/-/issues/177)).
    • Documentation sweep. openbsd/amd64 and openbsd/arm64 join the supported platforms table in the package documentation: both have been in the builder test matrix since January and are cross-built by make build_all_targets, but had never been listed. The vfs DSN query parameter — which names a VFS registered with SQLite, such as one returned by vfs.New — is now documented alongside the other DSN parameters on Driver.Open. The "Debug and development versions" section no longer describes a GO_GENERATE environment variable and a go generate that this repository has not had since generator.go moved to modernc.org/libsqlite3; it now points at that repository and make vendor instead, and the stale //go:generate directive naming the removed file is dropped with it. modernc.org/sqlite/vec and modernc.org/sqlite/vfs gained the package doc comments they were missing, so both finally carry a synopsis on pkg.go.dev. Documentation only; no behavior changes.
    • Add NewConnector, returning a database/sql/driver.Connector for use with sql.OpenDB. It opens the same connections sql.Open("sqlite", dsn) does, from the same registered driver, so every function, collation, connection hook and virtual table module registered through this package applies to them. It exists for callers that need to interpose on the physical connections database/sql opens — tracing, metrics, connection-scoped setup — which sql.Open gives no access to: such a caller can embed the returned Connector, override Connect, and pass its own wrapper to sql.OpenDB. Previously the only way to reach the registered driver was the db, _ := sql.Open("sqlite", ""); drv := db.Driver(); db.Close() idiom, which works only because sql.Open does not connect and this driver does not implement driver.DriverContext; and the only way to get a wrapper into a *sql.DB was sql.Register, which is process-global, panics on a name it has already seen, and cannot be undone, so a library had to invent a unique driver name per configuration. sql.OpenDB registers nothing. Constructing a &sqlite.Driver{} is not an alternative — its fields are unexported, so it carries none of the registrations. NewConnector checks the DSN only as far as it can without opening a database — a query string that does not parse, and conflicting vfs parameters; everything else continues to be validated when the connection is opened, so an unknown parameter or an out-of-range value is reported by Connect rather than at construction. Nothing about the existing sql.Open path changes: *Driver deliberately still does not implement driver.DriverContext, so sql.Open remains lazy and DSN errors continue to surface where they always have. A runnable sample is in examples/connector. Resolves [GitLab issue #253](https://gitlab.com/cznic/sqlite/-/issues/253), thanks Alessandro Segala (@​ItalyPaleAle)!
    • Document that a caller-constructed sqlite.Driver is not the driver this package registers as "sqlite". Its fields are unexported, so it starts with no functions, collations or connection hooks and the only way to give it any is its own RegisterConnectionHook method; the package-level Register* functions always apply to the registered driver. Connections such a Driver opens therefore run without the package-level functions and collations — and because a registered function silently replaces a SQLite built-in of the same name, a Driver you construct can evaluate upper(x), date(x) and the like differently from one opened through sql.Open. Virtual table modules are the one exception: they are held process-globally and reach every Driver. Constructing one remains supported for the private-hook pattern — a driver registered under a name of its own with sql.Register so its connection hooks apply only to its own connections — and is otherwise best avoided in favour of sql.Open or NewConnector. Documentation only; no behavior changes.
  • 2026-07-20 v1.55.0:

    • Add github.com/mattn/go-sqlite3-compatible shorthand DSN query parameters to ease migration from that driver: _busy_timeout/_timeout, _foreign_keys/_fk, _journal_mode/_journal, _synchronous/_sync, _auto_vacuum/_vacuum, and _query_only, each setting the correspondingly named PRAGMA. Values are validated against the same set mattn/go-sqlite3 accepts (case-insensitive) and an unrecognized value fails the connection with an error, so a typo such as _synchronous=fu1l or _foreign_keys=yes_please is reported rather than silently downgrading durability or dropping foreign-key enforcement. The keys are applied in a fixed order independent of their order in the DSN — _busy_timeout and _auto_vacuum before any _pragma values (auto_vacuum must be set before the database is first written), the rest after, and _query_only last — and where a key and its alias are both supplied the alias wins, matching mattn/go-sqlite3; selection is by presence rather than by value, so supplying the alias empty (_foreign_keys=on&_fk=) suppresses the PRAGMA rather than deferring to the primary key, again matching that driver. Behavior change to note: prior releases ignored these keys entirely, so a DSN carried over from a mattn/go-sqlite3 setup changes in two ways. A recognized key that previously did nothing now takes effect — _foreign_keys=on begins enforcing constraints against data that may already violate them, _journal_mode=wal persistently converts the database file, and _query_only=1 makes the connection read-only. And a value outside the accepted set now fails the connection with an error where the same DSN previously opened successfully — for example a duration-style _busy_timeout=5s or _timeout=5000ms, neither of which is the integer that key requires. Review such DSNs before upgrading. _pragma is unchanged and no pre-existing parameter changes meaning, though see the following entry for a change in when all of them are validated.
    • See [GitLab merge request #134](https://gitlab.com/cznic/sqlite/-/merge_requests/134), thanks Toni Spets (@​beeper-hifi) and Ian Chechin!
    • Validate every DSN query parameter before applying any of them. Parameters were previously checked as each was reached, so a DSN whose later parameter was rejected had already executed the PRAGMAs ahead of it. Because PRAGMA journal_mode and PRAGMA auto_vacuum are persistent changes to the database file, a DSN such as file:x.db?_journal_mode=wal&_synchronous=bogus failed the connection and yet left x.db converted to WAL. A failed Open now leaves the database as it found it. This covers the pre-existing _txlock, _timezone, _time_format, _time_integer_format, _inttotime and _texttotime parameters as well as the shorthand keys above: all of them were validated only after the _pragma list had already run, so the same DSN shape — a valid _pragma=journal_mode=wal alongside a misspelled _txlock — converted the file before reporting the error. Only the values accepted for each parameter are unchanged; a DSN that opened successfully before still opens, and one that failed still fails with the same error. _pragma remains the sole exception, since its values are executed verbatim and cannot be checked in advance: a malformed _pragma is still rejected by SQLite as it runs, after any earlier _pragma in the list has taken effect.
  • 2026-07-15 v1.54.0:

    • Upgrade to SQLite 3.53.3. This also bumps the pinned modernc.org/libc to v1.74.1; as always, downstream modules must pin the exact same modernc.org/libc version this module's go.mod pins (see [GitLab issue #177](https://gitlab.com/cznic/sqlite/-/issues/177)).
    • Under the opt-in _texttotime DSN parameter, best-effort parse date-shaped TEXT values from columns SQLite reports with an empty declared type — aggregates and expressions over a date column (MAX(d), COALESCE(d, ...), upper(d), d || ''), subqueries, and typeless real columns (CREATE TABLE t(x)) — into time.Time, instead of delivering them as a raw string that Scan cannot store into a *time.Time. The existing declared DATE/DATETIME/TIME/TIMESTAMP path is unchanged; this only adds the empty-decltype case. The conversion is strictly best-effort: a value that does not parse as a time falls through to the original string, so no Scan that worked before can newly fail. ColumnTypeScanType continues to report string for empty-decltype columns, since the declared type cannot prove the column is temporal. Without _texttotime the behavior is byte-for-byte unchanged. Resolves [GitLab issue #248](https://gitlab.com/cznic/sqlite/-/issues/248).
    • See [GitLab merge request #133](https://gitlab.com/cznic/sqlite/-/merge_requests/133), thanks Ian Chechin!
  • 2026-06-21 v1.53.0:

    • Add experimental netbsd/amd64 support, resolving the long-standing build break in [GitLab issue #246](https://gitlab.com/cznic/sqlite/-/issues/246). This target is intentionally not yet listed among the supported platforms in the package documentation: the port had been broken for years and is only now revived, and there is as yet no real-world experience running it under production workloads. Green CI is not the same as battle-tested — so while the full test suite (including the pcache and vec packages and the -race concurrency test) passes on NetBSD 10.1 / Go 1.26.3, and the entire upstream toolchain (libc, cc, ccgo, libz, libtcl8.6, libsqlite3, libsqlite_vec) is green on the NetBSD CI builder, the target is offered for evaluation only. If you run NetBSD, please exercise it with your own workloads and report back via #246; the intent is to promote it to a fully supported platform after a period of broader real-world testing (on the order of a month) elapses without surprises.
    • Implementation notes: the previously shipped lib/sqlite_netbsd_amd64.go was a stale old-generator transpile that no longer compiled (the mu.enter/mu.leave break in #246); it is replaced by a fresh new-generator transpile consistent with every other platform, and modernc.org/sqlite/vec (sqlite-vec) is vendored and auto-registers on netbsd. Correct operation requires the matching pinned modernc.org/libc, which carries two NetBSD-specific fixes found during this work: the mmap(2) PAD-argument ABI (without it, concurrent WAL access faults with SIGBUS in the WAL-index shared memory) and a working abort(3) (the prior stub left SQLite's crash-recovery writecrash test unable to terminate by signal). As usual, downstream modules must pin the exact modernc.org/libc version this module's go.mod pins.
    • See [GitLab merge request #82](https://gitlab.com/cznic/sqlite/-/merge_requests/82), thanks Leonardo Taccari (@​iamleot) and Thomas Klausner (@wiz)!
    • Add experimental freebsd/386 and freebsd/arm support. As with the netbsd/amd64 target above, these two 32-bit FreeBSD ports are intentionally not yet listed among the supported platforms in the package documentation: freebsd/386 previously shipped a stale, effectively untested SQLite 3.41 transpile, and freebsd/arm is entirely new, so neither has real-world production mileage yet. Both are now freshly transpiled at SQLite 3.53.2 consistent with every other platform, build cleanly, and pass the full test suite (core, WAL/concurrency, and the vec package) on the FreeBSD CI builders; they are offered for evaluation only. If you run 32-bit FreeBSD, please exercise these targets with your own workloads and report back — the intent is to promote freebsd/386, freebsd/arm, and netbsd/amd64 to fully supported platforms in a future release cycle, once a period of broader real-world testing elapses without surprises.
    • Implementation notes: correct operation on freebsd/arm requires the matching pinned modernc.org/libc (v1.73.4), which fixes the per-arch mmap(2) off_t encoding for 32-bit FreeBSD; without it the WAL shared-memory mapping faults with SIGBUS under concurrent access, the same class of bug found on the netbsd port. As usual, downstream modules must pin the exact modernc.org/libc version this module's go.mod pins.
    • See [GitLab merge request #119](https://gitlab.com/cznic/sqlite/-/merge_requests/119), thanks Olivier Cochard-Labbé (@​ocochard)!
    • Add a Go-facing wrapper for SQLITE_CONFIG_PCACHE2. PageCache is the factory and Cache the per-database instance, both idiomatic Go interfaces; Page exposes the raw Buf and Extra pointers that SQLite reads through the C pcache contract. RegisterPageCache and MustRegisterPageCache install the module process-globally before the first sql.Open; subsequent Open calls are gated through a one-shot Xsqlite3_config(SQLITE_CONFIG_PCACHE2) so a too-late Register returns ErrPageCacheTooLate rather than silently falling through to the built-in pcache1. The binding owns the sqlite3_pcache_page stub and re-consults the implementation on every Fetch, reusing the stub only when the returned Page value is unchanged, which keeps a bounded/evicting purgeable cache safe by construction.
    • See [GitLab merge request #126](https://gitlab.com/cznic/sqlite/-/merge_requests/126), thanks Ian Chechin!
    • Add modernc.org/sqlite/pcache, the reference page-cache implementation that accompanies the #126 SQLITE_CONFIG_PCACHE2 wrapper. pcache.New returns a *Pool satisfying the PageCache interface; register it once with sqlite.MustRegisterPageCache(pcache.New()) and every connection opened afterwards draws its pages from it. Each Pool.Create mints a fresh per-database Cache: a bounded, LRU-evicting page store that honours the PRAGMA cache_size soft cap and releases the least-recently-unpinned page when it must make room. Page memory — the Buf and Extra buffers SQLite reads through — is allocated with libc.Xmalloc/libc.Xcalloc and therefore lives off the Go heap, which keeps SQLite's interior pointer arithmetic on the page extras from tripping the race detector's checkptr enforcement. Pool.Stats reports aggregate lifetime counters (hits, misses, allocs, evictions, rekeys, truncates, caches) across every cache a Pool has created, so hit/miss/eviction behaviour is observable without instrumenting individual caches. Cross-connection page sharing is out of scope for now; each Create returns an independent per-database cache.
    • Validated end-to-end against the #126 stress workload (cache_size=16, 4000 BLOB rows with DELETE and incremental_vacuum, integrity_check clean under -race) and benchmarked for the memory-utilization goal tracked in [GitLab issue #204](https://gitlab.com/cznic/sqlite/-/issues/204).
    • See [GitLab merge request #127](https://gitlab.com/cznic/sqlite/-/merge_requests/127), thanks Ian Chechin!
    • Tighten the modernc.org/sqlite/pcache reference implementation per cznic's !127 review follow-ups. Adds Stats.EasyRefusals, a per-Pool counter for the cases where FetchCreateEasy returns nil at cap; SQLite reacts to a refusal by spilling dirty pages and retrying with FetchCreateForce, so the new field is a direct proxy for the I/O pressure the strict Easy contract imposes vs pcache1's recycle-without-spill behavior. BenchmarkPoolEvictionChurn was reworked to drive a rotating-residue DELETE (k % 3 = i % 3) and re-insert a matching batch each cycle so the spill pressure recurs and easy-refusals/op scales with b.N instead of capping at the seed's one-time first-cycle cost; both existing benchmarks now report easy-refusals/op alongside the page-allocs/evictions metrics. Stats.Evictions documentation was tightened to match the actual behavior (counts LRU eviction, Unpin(discard=true), Shrink releases, and Unpin(discard=false) trimming back to target after a FetchCreateForce overcommit; bulk frees from Truncate, Rekey collisions, and Destroy are not counted). The TestPoolRoundTripIntegrity comment claiming the workload exercises xRekey ~15 times has been corrected; the SQL surface does not reliably emit xRekey here, and that codepath is covered by the unit tests instead.
    • See [GitLab merge request #130](https://gitlab.com/cznic/sqlite/-/merge_requests/130), thanks Ian Chechin!
    • Make modernc.org/sqlite/pcache -race-clean under SQLite's cache=shared mode. The pool already runs correctly under shared-cache because every callback into a given Cache is serialised internally by SQLite's sqlite3BtreeEnter on the BtShared mutex; verified empirically with a lock-free in-flight probe (max-in-flight = 1 on the canonical two-connection workload, 4 on a positive control with goroutines hitting the cache directly). However the Go race detector does not recognise SQLite's libc mutex as a happens-before edge and reports false-positive races on Fetch vs Unpin reads/writes of the per-cache state, which surfaces as DATA RACE failures for any user who registers the pool and runs their suite under -race. A sync.Mutex on the cache type is now taken on every public method (SetSize, PageCount, Fetch, Unpin, Rekey, Truncate, Destroy, Shrink), always. On the common non-shared-cache path the lock is uncontended (one atomic CAS per Lock/Unlock pair, negligible next to the SQLite work it bookends); on the shared-cache path it just rubber-stamps the order SQLite's BtShared mutex already established. A new e2e_test.go TestSharedCacheTwoConns_Integrity drives two sql.Conn against the same cache=shared URI with concurrent writers and asserts PRAGMA integrity_check = ok under -race; passes cleanly with the lock, would surface the false-positive without it. Design notes live in pcache/sharing.go.
    • See [GitLab merge request #131](https://gitlab.com/cznic/sqlite/-/merge_requests/131), thanks Ian Chechin!
    • Add a Go wrapper for sqlite3_db_status, the per-connection runtime counters (cache hit/miss/write/spill rates, schema and prepared-statement memory, lookaside usage, deferred foreign keys). DBStatus is an interface implemented by the driver connection and reached through the database/sql escape hatch (*sql.Conn).Raw(), mirroring the existing FileControl surface; DBStatusOp is a distinct typed enum of the SQLITE_DBSTATUS_* verbs so a counter from a different op family will not compile in its place. Status(op, reset) returns the (current, high) pair and optionally resets the counter. This also lets modernc.org/sqlite/pcache measure real I/O instead of the EasyRefusals proxy: the new BenchmarkPoolSpillIO reads the pager-level SQLITE_DBSTATUS_CACHE_SPILL/_CACHE_WRITE counters, which the pager maintains identically for pcache1 and the pool, making the pcache1-vs-pool comparison cznic raised on the !127 review a genuine apples-to-apples measurement. On the rotating-residue eviction-churn workload at cache_size=16 the pool spills ~3.5x more than pcache1 (cache-spill/op 31.96 vs 8.96) for ~3% more page writes (cache-write/op 450 vs 436) at identical hit/miss, quantifying the I/O cost of the strict Easy contract that EasyRefusals only proxied.
    • See [GitLab merge request #132](https://gitlab.com/cznic/sqlite/-/merge_requests/132), thanks Ian Chechin!
    • Add an opt-in _dqs DSN query parameter that disables SQLite's double-quoted string literal compatibility quirk on a per-connection basis. When _dqs=0 (or any strconv.ParseBool false value) is supplied, the driver calls sqlite3_db_config with SQLITE_DBCONFIG_DQS_DDL and SQLITE_DBCONFIG_DQS_DML set to off before any statement is prepared, so a double-quoted identifier that fails to resolve raises a parse error instead of silently falling back to a string literal. Absence of the parameter, or _dqs=1, leaves SQLite's default behavior unchanged; existing DSNs continue to work byte-for-byte. Resolves [GitLab issue #61](https://gitlab.com/cznic/sqlite/-/issues/61).
    • See [GitLab merge request #128](https://gitlab.com/cznic/sqlite/-/merge_requests/128), thanks Ian Chechin!

... (truncated)

Commits
  • 6e86ac4 doc.go, CHANGELOG.md: promote freebsd/386, freebsd/arm and netbsd/amd64
  • 47d0960 Merge branch 'driver-registration' into 'master'
  • 9ed2aad CHANGELOG.md: document the per-Driver registration methods
  • 20e2e17 sqlite: let a caller-constructed Driver register its own functions, collation...
  • 15039fd all_test: make TestConnectionHook survive -count>1
  • 224fef6 all_test: drop a trailing space gofmt flags
  • 50ee6dd vendor_libs: handle a deduplicated libsqlite3/libsqlite_vec checkout
  • 15ca503 licensing: ship the sqlite-vec MIT notice, normalize the license names
  • 69cd3ca GOVERNANCE.md: add Ian Chechin as maintainer
  • 198be3c all_test: tolerate a cgo-less toolchain in the recursive -race check
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.56.0 to 1.57.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.56.0...v1.57.0)

---
updated-dependencies:
- dependency-name: modernc.org/sqlite
  dependency-version: 1.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot @github

dependabot Bot commented on behalf of github Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Labels

The following labels could not be found: dependencies. Please create it before Dependabot can add it to a pull request.

Please fix the above issues or remove invalid values from dependabot.yml.

treeol added a commit that referenced this pull request Aug 26, 2026
* docs(card148): add wakild foundation design doc, Mashura-gated plan, D10 inventory

Card #148 (https://trello.com/c/Ba4YYGXM) planning artifacts on
feature/wakild-daemon:

- docs/design/wakild-foundation.md — the wakild daemon foundation design doc,
  verbatim from the user (command/event split, Connect API, tenancy from day
  one, SQLite+kvr storage split, P0-P5 phases).
- docs/cards/card-148-wakild-impl-plan.md — FINAL implementation plan, gated
  by Mashura (3 panels). Key decisions: P0 is daemon-shaped (non-blocking
  SubmitInput), two event classes, store-pluggable sequencer, tenancy from P0,
  sync->async approval shim in P0, Service split, P1 replay = projection not
  event sourcing.
- docs/cards/card-148-d10-inventory.md — P0 chunk 1: full inventory of every
  direct *agent.App access from internal/tui and cmd/wakil (~130 accesses,
  7 output channels, 27 sendEvent sites, 1 channel-bearing message).

No code changes in this commit.

* feat(core/event): add transport-free domain event model (card #148 P0 chunk 2)

First code of P0. internal/core/event is the domain event vocabulary every
client will consume; it lives under internal/core so that core never imports
api/gen or internal/server (foundation doc §2.1).

Implements plan decisions D2/D4/D6:
- Two event classes (durable replayable vs ephemeral live-only), derived from
  Kind; MessageCommitted is the durable counterpart of MessageDelta.
- Tenancy from day one: TenantID/SessionID on every envelope, embedded
  tnt_local/usr_local principals; no empty tenant passes.
- Events are data-only: single envelope + typed payload, no channels/callbacks.
  Kind/payload pairing enforced against an exact reflect.Type registry (not
  name strings, so same-named types from other packages or proto can't match).
- Draft vs committed lifecycle: ValidateDraft (Seq==0) vs ValidateCommitted
  (durable Seq>=1, ephemeral Seq==0), so the appender has a clean pre/post
  contract before the store exists.
- Typed prefixed IDs (tnt_/usr_/wsp_/ses_/trn_/tcl_/apr_/sub_) with Validate
  methods, called from the envelope AND payloads; checkID panics on unknown
  kind (programmer error) instead of silently passing.

Mashura (3 panels) reviewed this chunk; folded in: draft/committed split,
ephemeral Seq==0 enforcement, reflect.Type registry, typed-nil pointer
rejection, payload ID/enum validation, checkID unknown-key guard, registry
completeness test.

Verified: go test + go test -race pass, go build ./... clean, go vet clean,
gofmt clean.

* feat(core): add session service contracts + typed UUIDv7 id generation (card #148 P0 chunk 3)

Introduces the transport-free core contract surface (plan D3/D4/D7) that
chunks 4-8 and P1/P2 build on, plus prefixed UUIDv7 id generation.

internal/core (package core, service.go):
- Three behavior-first interfaces (D7): SessionService (commands),
  EventReader (Subscribe/ListEvents), SessionReader (queries/snapshot).
  Bootstrap (config, proxy.Client, executor, sinks) stays OFF the interfaces.
- Principal identity model (D4): typed tenant/user + role + scopes +
  auth_method; EmbeddedPrincipal() returns a fresh value (no mutable global).
- Session state machine with an explicit transition table + CanTransitionTo.
- Store contracts (D3): EventAppender (durable-append + sequence assignment,
  atomic, commit->notify point; rejects ephemeral drafts) and EventLog
  (cursor-addressable durable read). Sequencer is NOT exposed as a seam --
  assignment folds into Append so contiguous sequences can't be violated.
- ApprovalOutcome enum (deny/allow_once/allow_reads_once) instead of a
  representable-ambiguous bool pair; decision carries session correlation.
- SubmitInput contract pinned: FIFO enqueue (bounded), TurnAck = acceptance
  not completion, error-session re-drive via SubmitInput, non-blocking.
- Subscription lifecycle contract (handoff, dedup-by-seq, Next->io.EOF after
  Close, backpressure) and a fuller error vocabulary (state transition,
  approval not-found/already-resolved, subscription closed).

internal/core/id:
- Generator producing event-prefixed UUIDv7 ids (tnt_/usr_/wsp_/ses_/trn_/
  tcl_/apr_/sub_); NewFromReader for deterministic tests; package-level
  helpers on crypto/rand. Promotes github.com/google/uuid v1.6.0 to a direct
  dependency.

Verified: go build ./... + go vet clean; go test -race ./internal/core/...
green; full go test -count=1 ./... green across all packages; gofmt clean on
tracked files (CI gate). Mashura review (3 panels) folded in: Sequencer seam
removed, ephemeral-append rejection, approval enum, state-transition table,
EmbeddedPrincipal as function, consistent explicit principal args, submit
enqueue-vs-busy resolution, subscription lifecycle contract, not-found vs
not-authorized resolution, honest SessionSnapshot framing. Known seams noted
in docs, not implemented: tenant-isolation is service-layer (plan D4/P1 note),
pagination/retention, idempotency enforcement (RequestID reserved), P1
session-creation fields (additive).

Refs: https://trello.com/c/Ba4YYGXM

* feat(core): add in-memory session host (card #148 P0 chunk 4)

Implements deliverable 3 of the P0 plan and all three service interfaces
(SessionService, EventReader, SessionReader) over the D3 store contracts.

- One executor goroutine per session with a bounded FIFO input queue;
  SubmitInput is genuinely non-blocking and returns TurnAck{TurnID}.
- Turn finalization is a single lock-protected linearization point
  (finishTurn), so a close or interrupt racing a turn's return can never be
  lost or park the session in a stuck state.
- Interrupt/CloseSession cancel via an internal context and emit
  TurnCompleted{cancelled} / SessionClosed — never a silent abort.
- Durable event log (MemLog) with contiguous, atomic append-as-one-step
  sequencing; the executor is the single commit->notify point.
- Subscribe registers-before-replays, stages replay history in an unbounded
  seq-ordered pending queue, and merges concurrent live commits without
  order inversion or loss. Slow subscribers are disconnected with
  ErrSubscriptionGap (never block the executor; durables never silently lost).
- Turn output is committed as MessageCommitted; acked-but-never-run inputs are
  always explicitly abandoned with TurnCompleted{cancelled} (never silent).
- Crash-recovery stub RecoverRunning: recovered sessions enter error state
  with a SessionError{daemon_restart} marker.
- Tenant isolation (no existence leak) and role gating on every method.

Verified: build/vet clean; go test -race -count=5 on sessionhost green;
full go test -count=1 ./... green across all 27 packages; gofmt clean.

Mashura review (card #148 chunk 4): two panels converged on blocking defects
(finalization race, replay/live reordering + lossy catch-up, discarded turn
output, subscription detach leak); all folded in.

Trello: https://trello.com/c/Ba4YYGXM

* docs(card148): add Mashura-reviewed chunk-5 plan (event-emission seam + agent adapter)

Card #148 P0, workflow step 4: plan for chunk 5 (deliverables 4 + 6).
Reviewed by three panels (gpt-5.6-sol, claude-fable-5, glm-5.2); feedback
folded in: serialized append->notify (emitMu), turn fencing + late-emission
contract, internal_error classification, ctx-aware approval confirmer with
full outcome fidelity, TurnInput.UserID resolver identity, authoritative
message text = TurnOutcome.Text, tool/subagent durable events deferred.

* feat(core): add turn-scoped event emitter + agent-loop adapter (card #148 P0 chunk 5)

Implements plan deliverables 4 + 6: the command/event boundary's outbound
half. A turn can now emit intermediate domain events (not just its flat
text/error result), and a real *agent.App turn drives an in-memory host
session end-to-end through SessionService/EventReader only.

sessionhost:
- TurnInput.Emit: turn-scoped Emitter (durable Emit + ephemeral Notify),
  concurrent-safe, fenced at finalization.
- s.emitMu serializes durable append->notify across ALL producers (executor
  terminals + worker-emitted events) so subscribers observe exact increasing
  Seq order.
- Fence check runs INSIDE emitMu against finishTurn's terminal emit, so a
  turn-emitted durable event can never append after its TurnCompleted
  (terminal ordering).
- Host-owned kind allowlist; ErrEmitFailed + ErrInternal classify emit/store
  failures as SessionError{internal_error}, not backend_failure.
- TurnInput.UserID carries the submitter principal for audit identity.

internal/wiring (new):
- HostTurnFunc: installs agent Out/OnReasoning/Confirm callbacks, runs the
  SendOutcome -> WaitForAsyncCompletion -> Resume loop, restores callbacks
  panic-safe. Bound to ONE host session (single-App, single-session rejected
  loudly). MessageDelta/ReasoningDelta emissions; authoritative text returns
  TurnOutcome.Text.
- hostConfirmer (D5 shim): ApprovalRequested -> ApprovalResolved with full
  approve/decline/allow-reads fidelity; resolver runs in a goroutine raced
  against ctx cancellation (stuck resolver cannot hang Interrupt/Close);
  emit failures latch into the turn error (no orphaned/duplicate approvals).

Docs: MessageDelta presentation-streaming caveat; ApprovalResolved.Resolver
now populated in P0; SubmitInput.ReadAction doc corrected; plan fence
wording corrected (linearizes before terminal append).

Reviewed by three panels (gpt-5.6-sol, claude-fable-5, glm-5.2): fold-ins
from both plan review and implementation review are applied.

Verified: go build/vet clean; go test -race full suite green (28 pkg);
go list structural gate clean (internal/core imports no bubbletea/server/
agent/tui/api-gen; wiring is the sole bridge). Trello: card #148.

Ref: https://trello.com/c/Ba4YYGXM

* docs(card148): chunk 6 plan — interim TUI→App control seam (deliverable 5, step 1/2)

* feat(card148): chunk 6 — interim TUI→App control seam (deliverable 5, step 1/2)

Introduce Control (user/session commands) and StateApply (round-trip runtime
results) interfaces in internal/agent, implemented by *App, and route all 20
TUI mutation sites through them. Fix ResumeSessionMsg + AppendSystemMessage to
take convMu (pre-existing lock bypass). Add a heuristic structural guard test
(narrowed: field writes rooted at m.app + seam-method calls through m.app +
enumerated pass-*App set) plus a negative test proving the guard is not
vacuous, and fake-Control/StateApply routing tests proving the seam is used.
D12 + deliverable-5 completion remain red (TUI still holds *agent.App; the
turn-driving re-route is the next chunk).

* docs(card148): chunk 7 plan — headless turn-driving re-route (Mashura-gated)

* feat(card148): chunk 7 — headless single-task re-route through session host (deliverable 5 step 2, exit gate #2 partial)

* docs(card148): chunk 7b plan — TUI re-route off *agent.App (Mashura-gated v2, gate #1)

* feat(card148): chunk 7b1 — agent-free facade contract + appOwners release path (Mashura-gated)

7b1: server-side foundations for closing Gate #1 (remove *agent.App from
internal/tui). No TUI cut yet — Gate #1 stays red; the TUI keeps driving
*agent.App until 7b3.

sessionclient (NEW package, internal/core/sessionclient):
- Agent-free facade interface + neutral DTO inventory (D26)
- Imports only event, proxy, core — never internal/agent
- go list -deps verified: zero internal/agent in transitive graph
- DTOs: Consent, ContextLimit, Backend, OpID, SessionSummary, SessionScope,
  ApprovalChoice, ApprovalRequest, ClientSnapshot, WorkflowSnapshot,
  CommandResult, RotateRequest, RepoStateMutator, CompletionSource
- CommandResult replaces agent.HandleTUICommand's (handled, quit, cmd)
  return — no agent.Cmd/Msg leak; Validate() rejects contradictory states
- ClientSnapshot: immutable version-stamped view-model (D26), not live getters
- Full Facade interface: SessionService + EventReader + TUI-specific ops +
  client-initiated mutations (D26 grounding #11) + side questions (D29) +
  slash-command dispatch (D23) + session listing + lifecycle

wiring/hostturn.go — appOwners release path (7b1):
- appOwners map: struct{} → *hostTurn (tracks the owner)
- HostTurnHandle: bundles TurnFunc + Release() + App()
- NewHostTurnHandle: factory entry point returning the handle
- HostTurnFunc: kept as thin wrapper (headless backward compat)
- Release(): idempotent, rejects active turns (ErrTurnActive)
- run(): atomically sets turnActive; rejects second concurrent turn and
  released-before-start (defense-in-depth + concurrency fix from Mashura review)

Tests:
- sessionclient: structural agent-free guard (go list -deps), ContextLimit
  parity, CommandResult.Validate
- wiring/factory_test.go: claim/release/reclaim lifecycle, idempotent release,
  active-turn rejection, released-turn rejection, concurrent claim/release
- All green under -race (29 packages)

Mashura review op-12 (gpt-5.6-sol) findings addressed:
- Critical: run() now rejects a second concurrent turn atomically
- doc.go: 'event and proxy only' → 'event, proxy, and core'
- TestPackageIsAgentFree: real go list -deps guard (not a placebo)
- CommandResult.Validate + RotateRequest.Validate added
- Rotation rationale clarified: fresh App pointer; Release is cleanup signal

* feat(card148): chunk 7b2 — session-scoped emitter + async approval + new event kinds (Mashura-gated)

Event package (D24/D28/D29):
- 10 new event kinds: user_message_committed (durable, replay truth),
  conversation_compacted, workflow_turn_started, workflow_final_review,
  async_job_started/completed, side_question_completed (all durable),
  tok_rate, async_job_progress, side_question_progress, learn_nudge
  (all ephemeral)
- OpID type with op_ prefix, validation, constructor
- TurnCompleted gains Warn + WorkflowWillContinue fields (D28)
- All new payloads with Validate() methods
- Registry + completeness + validation tests updated

Session host (D24/D25):
- SessionEmitter interface: session-scoped emitter fenced at session
  close (not turn close) — legal for detached work after turn completion
- sessionEmitter concrete impl: rejects host-reserved, turn-scoped,
  and ephemeral kinds on Emit; accepts ephemeral on Notify
- turnScopedKinds allowlist: approvals, tool calls, subagent events
  rejected by session emitter to preserve terminal turn ordering
- UserMessageCommitted emitted from executor goroutine (handleInput)
  not SubmitInput — guarantees FIFO ordering invariant
- UserMessageCommitted in hostReservedKinds (host-owned)
- ParkApproval hook on TurnInput: parks session in awaiting_approval,
  blocks turn goroutine on decision channel raced with ctx
- Real RespondToApproval: validates pending approval, resolves via
  buffered channel, idempotent same-outcome duplicates
- Cancel-during-approval: ctx cancellation → forced decline before
  emitter fence
- Resolver identity: ApprovalResolved.Resolver records who actually
  answered (principal.UserID), not just the turn submitter

Wiring adapter (D25):
- WithAsyncApproval() option: TUI uses async park+resolve path;
  headless keeps sync inline resolver (parity unchanged)
- app.OnTokRate wired to SessionEmit.Notify(KindTokRate)
- app.EventSink wired to session-scoped surface (projection TODO 7b3)
- ApprovalResolved emit failure now fails closed (return false, not
  just log) — safety invariant

Tests (14 new, 2 updated):
- Session emitter: legal-after-turn, fenced-at-close, rejects-host-reserved,
  rejects-ephemeral-emit, rejects-turn-scoped-kinds, notify-accepts-ephemeral
- Async approval: round-trip, cancel-during-approval, not-found,
  already-resolved, wrong-id, concurrent-park-and-resolve (race)
- Headless sync parity: unchanged
- Event: OpID validation, new payload validation, updated kind class test
- Updated TestSubscribeReplayOverlap for UserMessageCommitted event

Pre-existing flaky test TestCloseSessionEmitsSessionClosedAndIsIdempotent
confirmed flaky before and after changes (state-before-event race in test).

Mashura review (op-14, gpt-5.6-sol): 5 critical findings addressed
(host-reserved UserMessageCommitted, executor-goroutine ordering,
resolver identity, fail-closed audit, turn-scoped kind rejection);
D28 workflow emission and D29 EventSink projection deferred to 7b3
per plan scope.

* fix(card148): 7b2 prerequisite fixes — detached callback ownership, approval resolver identity, SetAllowReads ordering, event projection scaffold (Mashura-gated)

Three 7b2 bugs identified by Mashura review (op-17, 3 panels) fixed:

1. Detached event delivery broken (hostturn.go): hostTurn.run saved/restored
   app.EventSink and app.OnTokRate per-turn. Between turns, the sink reverted
   to main.go's globalProg.Send — detached async jobs/side questions lost the
   session-scoped emitter. Fix: session-scoped callbacks (EventSink, OnTokRate)
   are now installed permanently on the first turn and NOT restored. Only
   turn-scoped callbacks (Out, Confirm, OnReasoning) are restored per-turn.
   The hostTurn struct gains a sessionEmit field that persists across turns.

2. Approval resolver identity (hostturn.go): forced decline on cancel/interrupt
   used in.UserID (submitter) instead of a system principal. D25 says
   cancellation records the system/interrupt principal. Fix: add
   event.SystemUserID and use it for forced declines.

3. SetAllowReads before durable emit (hostturn.go): app.SetAllowReads(true)
   was called before ApprovalResolved was durably emitted. If the append
   failed, consent was mutated but the turn failed. Fix: move consent mutation
   AFTER successful durable emit.

4. Event projection scaffold (projection.go): the app.EventSink was a no-op
   (TODO(7b3)). Added projectAgentEvent with the full mapping structure
   (skeleton — projection implementations land in 7b3 m2). This is the legal
   emit path for all agent message types through the session-scoped emitter.

TestCallbackRestore updated: OnTokRate/EventSink are now session-scoped
(permanent, not restored). Only Out/Confirm/OnReasoning are turn-scoped.

Refs: card-148-chunk7b-plan.md D24/D25, Mashura op-17 (gpt-5.6-sol,
claude-fable-5, glm-5.2)

* feat(card148): 7b3 m1 — neutral format package, ConversationManager interface, snapshot fixes, async command mechanism, mapping matrix

m1 (Contracts & Lifecycle):
- internal/core/format: extracted ShortID, Truncate, DerefStr, Indent,
  Yellow, StrPtr, TranscriptSize from internal/agent so agent-free
  packages (sessionclient, wiring, TUI) can use them without importing
  internal/agent. Includes agent-free structural guard test.
- ClientSnapshot: added OutputMode field (was missing), added slice
  immutability documentation and test.
- CommandResult: added OpID field for async commands (/handoff,
  /remember, /recall) that need event-based completion notification.
- ConversationManager interface: agent-free, sits above facade, handles
  /new, /resume, /handoff rotation. Documents detached-job cancel policy.
- mapping_matrix.go: complete command/message mapping matrix documenting
  every agent.Msg type → event.Kind or CommandResult or ClientSnapshot
  field, plus slash-command → CommandResult mapping.
- Structural tests: OpID validation, OutputMode field, slice cloning,
  ConversationManager interface shape.

All tests pass: go build ./..., go vet, agent, TUI, cmd/wakil, core.

* feat(card148): 7b3 m2 — complete agent-message→domain-event projection

m2 (Host Capabilities & Projection):
- Complete projectAgentEvent in internal/wiring/projection.go:
  - SubagentStartMsg → KindSubagentSpawned (durable)
  - SubagentActiveMsg → KindSubagentProgress (ephemeral, [active] marker)
  - SubagentChunkMsg → KindSubagentProgress (ephemeral)
  - SubagentFinishedMsg → KindSubagentProgress (ephemeral, [finished:status])
  - SubagentDoneMsg → KindSubagentCompleted (durable, status from Err)
  - AsyncJobStartMsg → KindAsyncJobStarted (durable)
  - AsyncJobChunkMsg → KindAsyncJobProgress (ephemeral)
  - AsyncJobDoneMsg → KindAsyncJobCompleted (durable, status from Err)
  - SideQuestionChunkMsg → KindSideQuestionProgress (ephemeral)
  - SideQuestionDoneMsg → KindSideQuestionCompleted (durable, status from Err)
  - AgentDoneMsg.LearnNudge → KindLearnNudge (ephemeral, if non-empty)
  - ToolStartMsg/ToolResultMsg: dropped (turn-scoped path handles these)
  - SysNoteMsg/CompactedMsg/BackendCtxLimitMsg/ModelListUpdatedMsg/
    MCPReconnectedMsg/TokRateMsg: dropped (snapshot fields or client-local)

- ID mapping helpers: subagentIDFromChatID, opIDFromString, toolCallIDFromString
  (deterministic prefix-stripping: proxy ID body reused with domain prefix)

- 33 projection tests covering every message type, nil/closed emitter,
  ID helpers, error status derivation, dropped messages, unknown types.

All tests pass: go build, go vet, wiring package.

* feat(card148): 7b3 m3 — wiring facade, ConversationManager, event pump

m3 (Wiring Implementation & Event Pump):
- wiringFacade: implements sessionclient.Facade by bridging *agent.App +
  *sessionhost.Host. Delegates SessionService/EventReader to host, constructs
  ClientSnapshot from App fields, routes mutations to App methods.
- conversationManager: implements sessionclient.ConversationManager. Creates
  fresh *agent.App for each conversation, wires to host, handles /new, /resume,
  /handoff. Detached-job cancel policy on close.
- EventPump: goroutine driving EventSubscription.Next, delivers events to TUI
  callback. Handles subscription gap recovery (resubscribe from lastSeq),
  pump cancellation, rotation drain via Done() channel.
- ClientSnapshot.Costs changed to *proxy.CostTracker (CostTracker has a mutex,
  cannot be copied by value).
- interpretAgentMsg: translates agent.Msg from HandleTUICommand into
  CommandResult fields (Notice, Rotate, Submit, Compacted).
- Conversion helpers: toClientContextLimit, toAgentContextLimit, toClientBackends,
  toClientWorkflow.
- Tests: event pump delivery, idempotent stop, ctx cancel, interface satisfaction.

All tests pass: go build, go vet, core, wiring, tui, cmd/wakil.

* fix(card148): 7b3 m3 — Mashura review fixes (gap recovery, SaveRepoState, event pump ownership)

Fixes from Mashura op-20 review:
1. EventPump gap recovery: use errors.Is instead of == (wrapped errors)
2. EventPump lastSeq: accept initialSeq in constructor so gap recovery starts
   from the subscription's cursor, not zero (prevents full-history replay)
3. EventPump EOF: use errors.Is(err, io.EOF) for terminal error
4. SaveRepoState: initialize mutator from current repo state before callback
   so unset fields preserve existing values (no accidental zeroing)
5. wiringFacade: own the EventPump (not just subscription); Close stops the
   pump and drains it, not just the subscription

All tests pass: go build, go vet, wiring package.

* feat(card148): 7b3 m4a — workflow continuation via host enqueue hook, session notes, learn-nudge parity

Mashura review panel (3 panels unanimous) chose Option A: the adapter runs
HandleWorkflowTransition after a successful turn and enqueues the continuation
through a host-provided TurnInput.EnqueueInput closure — the TUI is passive.

- sessionhost: TurnInput.EnqueueInput hook; enqueueTurn (SubmitInput-equivalent
  acceptance semantics); finishTurn sets TurnCompleted.WorkflowWillContinue
  when a completed turn has queued work following it
- wiring/hostturn: post-turn HandleWorkflowTransition + enqueue + durable
  workflow_turn_started audit marker; TakeLearnNudge parity (agent accessor
  replicating the old RunTurn nudge computation) delivered as ephemeral
  learn_nudge
- event: KindSessionNote (ephemeral) + SessionNote payload — in-turn progress
  notes (wfProgNote, handoff progress, policy notices) projected instead of
  dropped
- projection: SysNoteMsg → ephemeral session_note
- tests: workflow continuation (2 turns + audit marker + WorkflowWillContinue),
  no-workflow control, session-note projection

* feat(card148): 7b3 m4a — side-question registry, SetWorkflow conversion, snapshot versioning

- facade: unique domain OpIDs for side questions (id.NewOpID generator added
  to core/id) with a facade-side OpID→CancelFunc registry; CancelSideQuestion
  looks up + cancels + removes; Close cancels all registered side questions
  (detached-job policy). Replaces the constant 'op_sq' pseudo-ID and no-op
  cancel stub.
- facade: SetWorkflow converts WorkflowSnapshot → workflow.WorkflowState
  (phase-name→enum mapping, workflowPhaseFromName); nil still clears.
- facade: snapshot revision counter — every facade-mediated mutation bumps
  Version so clients detect staleness; Snapshot.Title from Session.Label.
- tests: unique OpIDs + registry lifecycle, SetWorkflow round-trip through
  Snapshot, version increments across mutators

* feat(card148): 7b3 m4a — real HandoffConversation via RunHandoffPipeline, hashed workspace IDs

- agent: RunHandoffPipeline exported seam over performHandoff steps 1–4
  (validation, old-session save, recency-split summary generation,
  session-history indexing with fallback chain, durable handoff record);
  HandoffResult carries what the wiring layer needs (payload, continuation
  prompt, note, chat IDs)
- wiring: HandoffConversation now runs the real pipeline — saves old session,
  generates summary, creates new conversation seeded with the pinned handoff
  context (untrusted-delimiter framing), clears pending images, and enqueues
  the continuation turn via host SubmitInput when proceeding (host-enqueues
  policy; auto-start failure degrades to a startup note, not a failed rotation)
- wiring: workspaceIDFromConfig derives wsp_<sha256[:8]> of the effective
  workdir — stable per workspace, valid ID grammar (the raw-path stub failed
  validation for empty workdirs)
- tests: handoff seeding (pinned context, no image leak, new ChatID), empty-
  conversation guard

* feat(card148): 7b3 m4a — subagent event enrichment, InfoSnapshot DTO, SessionSummary.Turns

- event payloads: SubagentSpawned carries Backend/Model/ToolNames;
  SubagentCompleted carries Err/CostUSD/FilesChanged/Grounding labels/
  CtxSize/HardMaxBytes/UsedBackend; SubagentProgress carries structured
  early-finished fields (Finished/FinishedStatus/FinishedCostUSD/
  FinishedFilesN) instead of a '[finished:status]' text marker;
  AsyncJobCompleted carries Err. Client tab/info-panel parity with the old
  Subagent*Msg/AsyncJobDoneMsg fields.
- projection: maps the new fields through.
- sessionclient: InfoSnapshot DTO (narrow, immutable, defensive copies) —
  endpoint/identity, model+backend selection, prompt/config bits, context
  gauge, transcript stats, workflow label, MCP servers, SearXNG tools,
  grounding entries, costs. Facade.Info() interface method; SessionSummary.
  Turns() mirrors agent.SessionTurns for the resume picker.
- wiring: Info() implementation (mashuraPanelLabel moved from the TUI so the
  label is computed wiring-side); endpoints for completion.
- tests: Info snapshot content + copy semantics.

* fix(card148): 7b3 m4a — DispatchCommand handoff deferral + learn-text mapping

- DispatchCommand intercepts /handoff BEFORE agent.HandleTUICommand: arg
  validation + quick-fail emptiness guards run at dispatch (fast), and the
  result carries Rotate{Type:handoff, Proceed} WITHOUT executing the
  summarizer pipeline — HandoffConversation runs it exactly once. Fixes the
  double-pipeline (dispatch executing performHandoff AND the manager running
  RunHandoffPipeline) and the event-loop blocking (120s summarizer) from the
  synchronous cmd() call.
- Documented calling contract: slow commands (/handoff /remember /recall
  /compact) execute synchronously inside DispatchCommand; the caller (TUI)
  wraps the call in a worker goroutine — the AdaptCmd pattern.
- LearnTurnMsg → Submit 'learn this for next time' (the literal the old TUI's
  LearnTurnMsg handler submitted via RunTurn), NOT '/learn' (infinite
  redispatch). WFFinalReviewMsg → Submit 'continue' with rationale (the
  adapter re-runs HandleFinalReview at turn end in verify state).
- tests: handoff deferral (no pipeline execution, proceed/stop/usage
  variants), empty quick-fail, learn literal.

* feat(card148): 7b3 m4b-prep — tool-call events on turn emitter, facade-owned event pump, BootstrapTUI

- hostturn: turnEmit field — the EventSink closure routes ToolStartMsg/
  ToolResultMsg (turn-scoped kinds; the session emitter rejects them by
  design) to the live turn's emitter, stamped with the turn ID; everything
  else projects on the session surface. turnEmit set/cleared per turn.
- projection: ToolStartMsg → tool_call_started (ArgDigest = primary arg),
  ToolResultMsg → tool_call_completed (Result preview) — previously dropped
  with a wrong assumption ('the host emits them' — tools run inside the
  agent loop; nothing emitted them). The TUI running-tool status line now
  has a live wiring path.
- facade: Subscribe starts the facade-owned event pump (deliver callback =
  tea.Program.Send); StartEventPump begins delivery when the caller is
  ready. Facade interface updated accordingly.
- wiring: BootstrapTUI — manager + first conversation (fresh or --resume),
  subscription armed, cleanup closure; the m4c main.go entry point.
- tests: tool events through a real turn (ordering: start < done < turn
  completed; TurnID stamped), projection unit tests, BootstrapTUI fresh +
  missing-resume.

* feat(card148): 7b3 m4b-prep — wiring-side fixes for the TUI cut (Mashura op-32 review)

Wiring-side blockers and review findings fixed before the TUI cutover:

- WithAsyncApproval enabled on manager-built conversations (B1): the
  sync confirmer with nil resolver declined every approval — an
  interactive TUI could never approve a tool. E2E test proves the park/
  respond/complete cycle.
- hostTurn EventSink closure bugs (op-32): captured the FIRST turn's
  TurnID forever (later turns' tool events stamped wrong) and read
  turnEmit without the mutex (data race). Both fields now written/read
  under ht.mu as a consistent pair.
- interpretAgentMsg gaps (B2): BatchMsg recursion (/backend, /model —
  note + ctx-limit/model-list side effects now applied facade-side per
  D24 query-state), MCPReconnectedMsg applies rebuilt tools,
  ClipboardImageRequest sentinel → CommandResult.ClipboardImage,
  OpenResumePickerMsg → ResumePicker (was a bogus Rotate).
- /new, /reset, /resume intercepted in DispatchCommand (B3): the agent
  path mutated the OLD App (NewConversation + finalizeSessionHistory).
  Rotation now classifies only; the manager owns finalize-on-rotation
  (FinalizeSessionHistory exported) and old-App freshness.
- Facade.Close: CloseSession first (cancels in-flight turn → unblocks a
  PARKED approval — was a permanent goroutine leak on rotation), drain
  the pump (bounded), retry Release while the turn winds down.
- bumpVersion AFTER mutation (was before — new version + stale data
  window defeated the staleness check).
- BootstrapTUI subscribes at the durable HEAD, not seq 0 (replayed
  ApprovalRequested would pop dead confirm gates; replay duplicates
  hydrated snapshot state).
- InfoSnapshot: InfoPanelOpen + OracleLabel 'no key' env fallback moved
  wiring-side.
- ConversationManager.NewConversation gains a current-Facade param
  (rotation context for finalize).
- TUI: tui_msgs.go + session_view.go migrated off agent format utils to
  internal/core/format (first two of nine files; stage 1 of the cut).

* feat(card148): 7b3 m4b stage 2 — TUI read paths through facade Snapshot/Info

Model gains facade/manager/principal fields (wiring path), with the App
kept for the legacy path until stage 3 (one read source per file:
snapshot()/info() accessors return ok=false when facade is nil and
callers fall back).

Migrated files (wiring path reads facade; legacy path unchanged):
- info_panel.go: infoMainSegments/infoSubSegments/infoToolsSegments/
  infoGroundingSegments/costSegments/billedSegment/toggleInfoPanel —
  every App-internal read (Exec.Describe/Cwd, Cfg.Image/Oracle/SearXng,
  Client.BaseURL/ChatID/Grounding, MCP.Servers, Workflow.SidebarLabel,
  Costs) now comes from InfoSnapshot on the wiring path. OracleLabel
  'no key' fallback + InfoPanelOpen are wiring-side (committed earlier).
- tui_view.go: ctxSegment (limit/usage/transcript stats) and
  headerStatusInput (workflow label, backends, model/submodel, consent,
  rawTools) read Info()/Snapshot()/facade.Consent().
- complete.go: compSources() model method (backends/models/endpoints/
  mentionBase via snapshot+Info); fetchSessionShortIDs via
  facade.ListSessions; computeCompletion/computeSlashCompletion take an
  injected session fetcher (stays a pure function).
- resume_picker.go: SessionSummary DTO, facade.ListSessions reload,
  Turns() for row rendering; Enter routes through beginRotation on the
  wiring path (rotation scaffolding: rotationRequest/rotationMsg/
  beginRotation — build-new-first, old facade closed after replacement
  exists, per the op-32 review); legacy path keeps agent resume.
- NewTUIModelWithFacade: wiring-path constructor (nil-App base model,
  hydrates from snapshot.Conv + Info().InfoPanelOpen).

tui_msgs.go + session_view.go were already migrated in stage 1 (a80666e).

* feat(card148): 7b3 m4b stage 3 — wiring-path runtime cut (events, approval gate, send, rotation)

The TUI now runs end-to-end on the wiring path when facade-backed:

- tui_events.go: handleEventMsg switch on event.Kind — the D2 mapping.
  TurnStarted (state machine for host-initiated turns), MessageDelta/
  ReasoningDelta (stream accumulation, reasoning collapse), ToolCall-
  Started/Completed (status-line indicators; ArgDigest=command),
  ApprovalRequested/Resolved (async gate), TurnCompleted (finishWiring-
  Turn: flush, classification by Outcome, deferred /auto grants, queue
  flush gated on Outcome+WorkflowWillContinue, chime, clearWiringTurn-
  State incl. pendApproval safety clear), SessionError, Subagent*
  Spawned/Progress(Finished)/Completed, AsyncJob*, SideQuestion*,
  SessionNote, LearnNudge, Workflow* (passive audit notes), Compacted,
  UserMessage/MessageCommitted (replay truth — no live handling).
  Session guard: cached m.sessionID drops stale-pump events.
- tui_event_tabs.go: tab lifecycle (spawn/complete/job) keyed by domain
  IDs (sub_*/op_*), extracted as methods.
- Confirm gate: pendApproval{approvalID} + y/a/n/esc/ctrl+c → facade.
  RespondToApproval (non-blocking buffered chan); ctrl+c also Interrupt;
  already-resolved races tolerated.
- Send path: wiring branch submits via facade.SubmitInput (no RunTurn/
  startTurn); optimistic streaming state confirmed by TurnStarted;
  image chips reconcile against snapshot.PendingImages; mentions via
  Info().MentionBase. flushQueuedPrompt mirrors it.
- Slash dispatch: /-prefixed input → facade.DispatchCommand in a Cmd
  goroutine → commandResultMsg applied on the event loop (Notice/
  Submit/Rotate→beginRotation/ResumePicker/ClipboardImage/Compacted).
  Plain text no longer swallowed (slash-prefix guard).
- Rotation: beginRotation Cmd (manager op → close OLD facade after the
  replacement exists) → applyRotation swaps refs, rebuilds view state,
  THEN subscribes at the durable head + starts the pump (after the swap
  — events before it would hit the session guard). SetProgramSend
  installs tea.Program.Send without a package global in main.
- cancelTurn → facade.Interrupt; startSideQuestion → facade; Init
  startup note via local startupNoteMsg; mid-turn /auto consent via
  facade.
- E2E tests (fake facade): turn lifecycle, approval gate incl. ctrl+c,
  session guard, send path, queue-flush-vs-workflow-continuation.

* feat(card148): 7b3 m4c — main.go bootstrap reroute through the session host

cmd/wakil/main.go now runs the TUI on the wiring path (Gate #1, TUI
half): wiring.BootstrapTUI builds the ConversationManager + first
conversation (fresh or --resume), runs the TUI startup steps, and
subscribes the event stream; tui.NewTUIModelWithFacade carries the
facade/manager/principal; tui.SetProgramSend installs prog.Send and
rt.StartEventPump begins delivery. globalProg and the
app.EventSink=globalProg.Send line are gone — the pump is the only
runtime event path.

BootstrapTUI gains BootstrapTUIOpts carrying what main.go did inline:
--attach-image (pending images), RestoreRepoState (fresh conversations
only + ctx re-resolve, same caveat about literal restored strings),
counsel mode/max defaults, and staging/memory startup-note composition.
--resume/--resume-id resolution (workspace-scoped most-recent for bare
--resume) moved into main.go's bootstrap prelude; the manager restores
the transcript.

main.go keeps exactly two agent imports: PrintSessions (--list-sessions
short-circuit) and ShortID (diag log path) + LoadSessionScoped for bare
--resume resolution — all three move behind wiring/agent wrappers in
m4d's guard pass.

* feat(card148): 7b3 m4d — hard cut complete: TUI production code drops internal/agent (Gate #1)

The legacy App-backed path is deleted; the facade+event path is the only
runtime path. Local TUI messages (dotTickMsg, armTickMsg, subTabCloseMsg,
copiedMsg, clipboardImageMsg) moved from the deleted handleAgentMsg
switch into handleEventMsg's local-message section.

Removed: tui_agent_msgs.go (the 30-case agent-msg switch), adapter.go
(AdaptCmd — no agent.Cmds remain), tuiModel's app/control/apply/pendConf
fields, NewTUIModel(app) (newBaseModel + NewTUIModelWithFacade only),
startTurn (submit path owns the state flip), legacy branches in
info_panel/tui_view/complete/resume_picker (Snapshot/Info are the only
read sources), startSideQuestion/cancelTurn legacy paths, the legacy
converters.

Tests (~30 files) migrated to the event switch: evt(kind,payload,sid)
feeds, fakeFacade-backed models, domain-ID tab identities (sub_*/op_*),
rotationMsg for rotation semantics, TurnCompleted outcomes for the
AgentDoneMsg cases. Test helpers: fakeFacade (extended: consent mutators,
SetInfoPanelOpen, StartSideQuestion), newWiringModel, rotatedFake,
wiringTestInfo. Deleted: adapter_test, control_routing_test (seam
deleted), control_seam_test (seam deleted). Agent-package behavior tests
(cost recording, ctx-limit resolution, ProgWriter) keep their agent
imports — they test agent functions and are exempt from the guard.

Guard: TestNoAgentImport asserts go list -deps of internal/tui contains
no internal/agent package — Gate #1's TUI half is now compiler+test
enforced. main.go's residual agent uses (PrintSessions, ShortID,
LoadSessionScoped for bare --resume) are the remaining cmd-side gap.

Full suite + -race green: internal/... cmd/...

* fix(card148): 7b3 m4 — subscribe first-boot event stream after program construction

BootstrapTUI was called with deliver=nil (prog.Send does not exist at
construction time), and Subscribe is gated on deliver != nil — so on first
boot the facade never subscribed, StartEventPump was a no-op (no pump), and
turn events never reached the TUI. The host still ran every submitted turn
(billing the request), but MessageDelta/TurnCompleted were undelivered: the
optimistic streaming state never cleared and the answer never rendered
(the 'stuck streaming' live-test finding).

Fix: TUIRuntime.SubscribeLive subscribes at the durable head once the
tea.Program exists (mirroring the rotation path's lazy subscribe);
main.go binds prog.Send as the deliver callback between SetProgramSend
and StartEventPump. Regression test TestBootstrapTUISubscribeLive proves
turn events flow through a nil-deliver bootstrap + manual subscribe.

* feat(card148): 7b3 m4e — main.go drops internal/agent via wiring session wrappers (Gate #1 cmd half)

main.go was the last production file importing internal/agent. Three thin
package-level wrappers in internal/wiring/sessions.go (PrintSessions,
ResolveRecentSession, ShortID) now carry those call sites:

- --list-sessions and the session-log ShortID calls delegate verbatim
- bare --resume resolves through ResolveRecentSession, fixing a
  variable-shadowing bug from the half-applied rewire where the resolved
  id was silently clobbered by the outer empty id
- the cmd guard is tightened: internal/agent is banned in ALL non-test
  cmd/wakil files including main.go; internal/tui stays main.go-only
- guard proven non-vacuous (synthetic agent import in main.go fails the
  test); wrapper tests pin the scoped/all/error contracts main.go relies on

go build, go vet, and tests for cmd/wakil + internal/wiring all green.

* test(card148): P0 exit-gate certification tests — concurrent seq, dual-subscriber order, replay projection

Closes impl-plan §3 gates 4, 5 and 9:

- Gate 4: TestExitGateConcurrentSeqUniqueAndIncreasing — 8 concurrent
  SubmitInput producers + 8 detached workers emitting through the session
  emitter inside a turn; durable log must be unique + strictly increasing.
  Backpressure rejections (queue full) are counted, not failed.
- Gate 5: TestExitGateTwoSubscribersSameOrder — two subscribers from
  cursor 0 see the identical durable order, equal to ListEvents order.
- Gate 9 (D9): TestExitGateReplayReconstructsProjection — replay from
  ListEvents(0) reconstructs the user+assistant transcript and approval
  terminal state identically to a live subscriber (6-entry transcript,
  approval resolved "approved").

Also deflakes TestCloseSessionEmitsSessionClosedAndIsIdempotent: it
previously read the log right after observing state=closed, racing the
documented P0 window where state flips before SessionClosed is appended
(host.go package doc). The test now also waits for the event itself.

Gate 3 coverage mapped (no new tests needed): all six enumerated paths
have existing host/integration tests.

All green under -race (full sessionhost suite, -count=1, plus targeted
stability loop).

Ref: trello.com/c/Ba4YYGXM (card #148, P0 exit gate)

* feat(card148): 7c — headless --plan re-routed through the session host (Gate #2 fully green)

`wakil run --plan` no longer drives agent.App directly. The legacy workflow
loop (workflow_legacy.go, deleted) is replaced by host-driven turns with an
after-turn resolver in the adapter:

- New HostTurnFunc option WithPlanAutoAdvance: the resolver applies the legacy
  headless auto-advance policy when HandleWorkflowTransition pauses the
  workflow — present→implement, review force-skip (+ legacy warning record),
  implement step advance, resolver-owned final review on no-marker crossing.
- Exactly-one-of invariant (Mashura op-34/35): every completed plan turn
  yields one of {terminal WorkflowOutcome event, one queued continuation,
  error}; enqueue rejection is terminal in plan mode (never silently idle).
- Decline control latch on hostTurn (last-wins, legacy parity): a declined
  approval — tool OR oracle confirm, including one latched DURING the
  transition — terminates the workflow before another turn is enqueued.
  Cancellation declines (reason "cancelled") are excluded.
- New durable KindWorkflowOutcome (+payload/validation) and ephemeral
  KindWorkflowWarning; KindWorkflowFinalReview is finally emitted (plan mode
  only — TUI event stream unchanged).
- runPlanTask/runPlanSession/consumeWorkflowEvents in wiring: first submit is
  "continue" (byte parity), terminal records byte-identical to legacy
  (pass/declined/gaps/verify_failed/backend_failure+resume_id), no tokens
  record (legacy parity), consumer never reads app state.
- Fail-closed fixes from review: NewApprovalID failure now records
  emitErr (internal error) instead of a silent decline.

Tests: 6 new plan-host integration tests (full chain→gaps, backend failure
with resume_id, mid-workflow decline, verify_failed, PASS with live oracle
reviewing, decline reason capture) + the 115 existing cmd/wakil workflow
tests now exercising the host path (oracle default/no-oracle, confirmer
policy). All green under -race; full build + vet clean.

Plan: docs/cards/card-148-chunk7c-plan.md (v2, Mashura-gated: op-34 plan
review with 3 panels, op-35 implementation review; all blockers folded in).

Ref: trello.com/c/Ba4YYGXM (card #148, P0 — Gate #2 closed)

* feat(card148): P1a-P1e — SQLite event log + store-backed sequencer (D3)

P1a — Event payload codec (internal/core/event/codec.go):
- MarshalPayload/UnmarshalPayload using payloadTypes registry for type dispatch
- JSON encoding ("json-v1") as P1 interim; P2 will add proto
- UnmarshalPayload returns VALUE (not pointer) matching MemLog representation
- Rejects: nil, typed-nil, type mismatch, NaN/Inf, malformed JSON, invalid
  decoded payload
- 30-kind round-trip test + 7 edge-case rejection tests

P1b — Migrations (internal/store/migrations/):
- 001_init.sql: sessions + events tables with composite FK
  (tenant_id, session_id) → sessions(tenant_id, id), CHECK constraints
  (seq > 0, encoding IN json-v1, last_seq >= 0), UNIQUE(tenant_id, id)
- migrate.go: forward-only, per-migration transactions, version tracked in
  schema_migrations (owned by runner, not SQL file), idempotent
- 6 migration tests: fresh DB, idempotent, constraints, reopen, load, missing dir

P1c — SQLiteStore (internal/core/sessionhost/sqlstore/):
- Implements sessionhost.Store (EventAppender + EventLog — NO interface change)
- SessionCreated append: atomic session row + event (seq=1) in one transaction
- Non-SessionCreated append: tenant-qualified UPDATE + INSERT in one tx;
  unknown session → ErrSessionNotFound; tenant mismatch → ErrSessionNotFound
  (no existence leak)
- Read: cursor-exclusive, ORDER BY seq ASC, limit<=0 omits LIMIT clause,
  eager payload decoding via UnmarshalPayload, encoding dispatch
- LastSeq: 0 for nonexistent session
- modernc.org/sqlite, SetMaxOpenConns(1), WAL, foreign_keys=ON, busy_timeout=5000
- 15 unit tests including: concurrent seq uniqueness (-race), reopen durability,
  PRAGMA verification after reopen, pointer payload, duplicate SessionCreated

P1d — Shared store contract harness (internal/core/sessionhost/storetest/):
- RunContract: 8 subtests (append, ascending seq, LastSeq, concurrent, cursor,
  ephemeral rejection, replay reconstruction) run identically against both
  MemLog and SQLiteStore
- MemLog contract test + SQLiteStore contract test (with reopen durability)
- exit_gate_test.go UNCHANGED (still uses MemLog)

P1e — Cross-tenant isolation test (internal/core/sessionhost/cross_tenant_test.go):
- Verifies host-level authz: tenant B → all of ListEvents, GetSession,
  Subscribe, SessionSnapshot, SubmitInput, CloseSession, Interrupt,
  RespondToApproval return ErrSessionNotFound (not ErrNotAuthorized, not empty
  — per core/service.go "no existence leak" contract)
- ListSessions does not leak cross-tenant sessions
- Runs against MemLog (tests P0 host authz, not store-level behavior)

Plan: docs/cards/card-148-p1-chunk-plan.md (v2.1, 2 Mashūra review rounds:
op-40 → 11 blocking findings → v2; op-41 → 5 remaining → v2.1 addendum)

Verification: go build ./... ✅, go vet ./... ✅,
go test -race ./... 31/31 packages ok ✅,
exit_gate_test.go unchanged ✅

Refs: Trello card Ba4YYGXM

* feat(card148): P1f — wire SQLiteStore into daemon composition (TUI + headless)

Production now uses SQLiteStore instead of MemLog for the session-host event
log (card #148 D3). Both TUI and headless paths open a workspace-keyed
SQLite database at <wakil-data>/sessionhost/<short-key>/sessionhost.db.

Wiring:
- agent.SessionHostDBPath: workspace-keyed DB path (same pattern as
  MemoryDBPath/SessionHistoryDBPath)
- conversation_manager.go: NewConversationManager opens SQLiteStore at init,
  injects via WithStore for every newConversation. Best-effort: falls back
  to MemLog on open failure (logged to stderr).
- headless.go: headlessStoreOpts() helper opens SQLiteStore for both
  runSingleTask and runPlanSession paths. Same best-effort fallback.

Tests (sqlstore_restart_test.go):
- TestHostRestartRecovery: create session + turn → close host+store → reopen
  → verify events persist (SessionCreated, UserMessageCommitted, TurnStarted,
  MessageCommitted, TurnCompleted, SessionClosed). Second host creates a
  new session in the same store; independent seq counters, old session
  unchanged.
- TestHostWithSQLiteStore_SubmitAndConsume: full turn cycle through host
  with SQLiteStore; verify MessageCommitted persisted with correct text.

Verification: go build/vet ✅, go test -race ./... 31/31 ok ✅

Refs: Trello card Ba4YYGXM

* feat(card148): P2a — proto schema + buf toolchain (wakil.v1alpha1)

- api/proto/wakil/v1alpha1/: 5 proto files (event, session, event_service,
  session_service, system) defining the P2 wire contract
- Event oneof: all 33 event kinds (field numbers 10-40, reserved 41-50)
- Services: SessionService (8 RPCs incl DeleteSession), EventService (3 RPCs
  incl GetSessionSnapshot), SystemService (GetServerInfo, Health)
- buf.yaml (lint: STANDARD + PACKAGE_DIRECTORY_MATCH + PACKAGE_VERSION_SUFFIX,
  except RPC_REQUEST_RESPONSE_UNIQUE + RPC_RESPONSE_STANDARD_NAME for shared
  Session type), buf.gen.yaml (local protoc-gen-go + protoc-gen-connect-go)
- api/gen/ committed (reproducible: buf generate && git diff --exit-code clean)
- go.mod: +connectrpc.com/connect v1.20.0, +google.golang.org/protobuf v1.36.12
- P2 chunk plan at docs/cards/card-148-p2-chunk-plan.md (Mashura-reviewed,
  revised per 3-panel feedback: D5 already implemented, TurnFunc transport-
  free, principal server-side, fail-closed store, SessionSnapshot RPC added)

buf lint: pass | buf generate: no drift | go build ./...: pass | go vet: pass | go test -race ./...: 33/33 ok

* feat(card148): P2b — DeleteSession on SessionService + SessionSnapshot wire path

- DeleteSession added to core.SessionService interface (service.go:70 says P2)
- Host.DeleteSession: soft-delete — closes active session, marks deleted,
  excludes from GetSession/ListSessions; events remain for audit
- lookup() returns ErrSessionNotFound for deleted sessions (all methods reject)
- Double-delete = ErrSessionNotFound; cross-tenant = ErrSessionNotFound;
  viewer = ErrNotAuthorized
- SessionSnapshot already on SessionReader — no core change needed,
  proto GetSessionSnapshot RPC maps directly
- 5 new tests: excludes from queries, rejects operations, closes active,
  cross-tenant, viewer-not-allowed
- Full -race suite: 33/33 ok

* feat(card148): P2c — Connect server adapter (core↔proto bridge)

- internal/server/connect/: 7 files bridging proto wire contract to core
- event_conv.go: 33-kind core.Event ↔ proto Event converter (all payloads
  round-trip verified)
- session_handler.go: SessionService 8 RPCs (Create/Get/List/Delete/Submit/
  Approval/Interrupt/Close)
- event_handler.go: EventService 3 RPCs (StreamEvents server-streaming,
  ListEvents, GetSessionSnapshot)
- system_handler.go: SystemService (GetServerInfo, Health)
- errors.go: 10 core sentinels → Connect codes (NotFound, FailedPrecondition,
  ResourceExhausted, PermissionDenied, etc.)
- principal.go: server-side EmbeddedPrincipal (no client-supplied identity)
- server.go: Server bundling all 3 handlers, http.Handler mount
- converter_test.go: 33-kind round-trip test (all pass)

proto: added MessageCommitted (field 41) to oneof (was missing from initial
schema). buf lint + generate: clean. go build + vet: pass.

* feat(card148): P2d — wakild binary + Unix-socket transport

Daemon binary that serves the Connect API over a Unix socket:
- cmd/wakild/main.go: flag parsing (--socket, --ephemeral,
  --shutdown-timeout), config loading, workspace ID derivation,
  serve-until-signal lifecycle.
- cmd/wakild/server.go: fail-closed SQLiteStore open (unless --ephemeral),
  executor + App + HostTurnHandle with WithAsyncApproval, sessionhost.New
  with store, Connect server, Unix-socket listener (0600 permissions,
  stale-socket detection/unlink, in-use refusal, parent dir 0700),
  graceful shutdown (http.Server.Shutdown → host.Close → resource cleanup).
- cmd/wakild/signal.go: SIGTERM/SIGINT → context cancellation.
- 16 tests: socket lifecycle (stale/in-use/permissions/parent-dir/
  connect-accept), flag parsing, Health RPC + GetServerInfo RPC over
  real Unix socket (ephemeral + non-ephemeral), nil-safe shutdown.

P2d simplification: one App drives one session (HostTurnFunc's
single-App binding). The daemon serves one active session at a time.
Per-session factory (multiple Apps/hosts) is a P2e concern.

* feat(card148): P2e — remote client + TUI --daemon mode

Remote client architecture (internal/remote/):
- dialer.go: Unix-socket HTTP client for Connect-go service clients
  (Session, Event, System). Verifies socket exists and is connectable
  before building clients. Disables keepalive pooling for clean
  reconnects across daemon restarts.
- pump.go: RemoteEventPump consumes the StreamEvents server-stream RPC,
  converts proto→domain events via protoconv, deduplicates durable events
  by seq, and reconnects from lastSeq+1 on stream break. Stop cancels
  the stream context; Done channel for rotation drain.
- facade.go: RemoteFacade implements sessionclient.Facade by calling
  Connect RPCs for all SessionService + EventReader surfaces. TUI-
  specific surfaces (Snapshot, Consent, Info) are limited to what the
  daemon exposes — the conversation is projected from events, not read
  from agent.App. Slash-command dispatch handles /quit, /new, /resume
  client-side; the rest passes through as regular input.
- manager.go: RemoteConversationManager implements
  sessionclient.ConversationManager (New/Resume/Handoff/Close).
- bootstrap.go: BootstrapRemote mirrors wiring.BootstrapTUI — dials
  daemon, checks health, creates/resumes first conversation, subscribes
  event stream. Returns TUIRuntime (Facade, Manager, Principal).

Shared proto conversion (internal/protoconv/):
- event_conv.go + payload.go: 32-kind proto↔domain event oneof conversion
  extracted from internal/server/connect/event_conv.go. Used by both
  the server adapter (domain→proto for RPC responses) and the remote
  client (proto→domain for inbound events). Avoids duplication and
  drift. SessionToProto/SessionFromProto handle the Session message.
- internal/server/connect/event_conv.go refactored to delegate to
  protoconv (432 lines → 14).

Config + main.go wiring:
- --daemon flag: selects remote daemon mode (TUI dials wakild over
  Unix socket instead of embedding the agent loop).
- --socket flag: overrides the default socket path
  (/wakild.sock or ~/.local/share/wakil/wakild.sock).
- cmd/wakil/daemon_mode.go: RunDaemonMode wires the remote bootstrap
  into the TUI — mirrors main.go's embedded path but uses remote.Bootstrap
  Remote instead of wiring.BootstrapTUI.

Tests: 6 tests pass with -race — dialer (socket exists, missing, non-
socket), default socket path, event round-trip conversion, pump dedup
logic, facade slash-command dispatch. Full suite passes (all 29
internal/ packages green).

* feat(card148): P2f — integration tests + buf breaking CI

End-to-end daemon↔client integration tests (cmd/wakild/integration_e2e_test.go):
- 9 tests exercising the full wire path over Unix socket: CreateSession,
  SubmitInput + ListEvents (verifies 7-event sequence), SessionSnapshot,
  CloseSession, Interrupt (verifies cancelled outcome), ResumeSession,
  DeleteSession, StreamEvents (real-time pump delivery), NewConversation
  (rotation). All pass with -race.

buf breaking CI (.github/workflows/ci.yml):
- New proto-breaking job: buf lint + buf breaking against base branch.
  Uses bufbuild/buf-action@v1, fetch-depth: 0 for full git history.
  PRs compare against origin/master; pushes compare against HEAD~1.
  buf.yaml already configured with WIRE_JSON + WIRE breaking rules.

Fix: merge daemon_mode.go into main.go (P2e regression).
- daemon_mode.go imported internal/tui, violating the headless seam test
  (TestHeadlessNoAgentImport: internal/tui is main.go-only). Moved
  RunDaemonMode → runDaemonMode into main.go, deleted daemon_mode.go.

* fix(card148): client workspace ID must be hashed, not raw path

runDaemonMode was passing cfg.WorkDir directly as the workspace ID
(e.g. "/home/valon/coding/wakil") instead of the hashed form
("wsp_<16-hex>") that the daemon expects. CreateSession rejected it
with invalid_argument because the raw path doesn't match the
WorkspaceID.Validate() format.

Fix: export wiring.WorkspaceIDFromConfig (was unexported) and use it
in both runDaemonMode (client) and cmd/wakild/main.go (daemon),
removing the duplicate copy. This ensures both sides derive the same
"wsp_"-prefixed SHA-256 hash of the effective workdir.

* feat(card148): P3 — read-only web UI

Add a static web console served by wakild via --http-addr flag.
The browser speaks the same Connect API (HTTP/JSON) as the TUI:

- web/embed.go: //go:embed for static assets
- web/index.html: SPA shell (sessions list + live viewer)
- web/app.js: vanilla JS RPC client (protojson camelCase, oneof
  payload access, 500ms polling for live events, renders all 32
  event kinds incl. tool calls and subagent tree)
- web/styles.css: Tokyo Night dark theme

Daemon changes:
- cmd/wakild/main.go: --http-addr flag (TCP, empty = disabled)
- cmd/wakild/server.go: dual-listener (Unix socket + optional TCP),
  webHandler combining Connect RPC + static files
- internal/server/connect/server.go: HandlerWithStatic method

Tests (6): static files, GetServerInfo, Health, ListSessions,
SessionSnapshot (with tool-call verification = exit gate),
ListEvents cursor pagination.

Exit gate: a running session is live-trackable in the browser,
including tool-calls and subagent tree.

go test -race ./... 36/36 packages green.
buf lint + buf breaking clean. gofmt + go vet clean.

* feat(card148): P4a — auth + tenancy schema (migration 002)

New migration 002_auth_tenancy.sql creates the control-plane tables for
auth and multi-tenancy per design doc §4.3:

- tenants (id, slug, display_name, status)
- users (id, email, display_name, auth_subject, password_hash, status)
  with partial UNIQUE index on auth_subject (non-NULL values only)
- memberships (tenant_id, user_id, role) with composite PK
- api_tokens (id, tenant_id, user_id, name, token_hash UNIQUE, scopes,
  expires_at, last_used_at, revoked_at) — composite FK to memberships
- join_tokens (id, tenant_id, user_id nullable, role, token_hash UNIQUE,
  created_by, expires_at, used_at) — composite FK on (tenant_id, created_by)
  to memberships; nullable user_id for create-on-exchange tokens

Indexes on all FK columns. Explicit ON DELETE clauses (CASCADE/SET NULL/
RESTRICT). Default tenant (tnt_local), user (usr_local), and owner
membership seeded via plain INSERT (tables are new within this migration).

sessions.tenant_id stays app-layer validated (accepted gap: SQLite cannot
add FKs to existing tables without recreation).

Tests (11 total, all pass with -race):
- Updated: TestApplyFreshDB (checks all 8 tables, version >= 2),
  TestApplyIdempotent (dynamic count), TestApplyReopen (pragmas on reopen),
  TestLoadMigrations (checks versions 1+2)
- New: TestApplyAuthTenancyTables (bootstrap + foreign_key_check),
  TestAuthTenancyConstraints (CHECK on role/status/expires_at),
  TestAuthTenancyFKs (all FK paths incl. composite FKs),
  TestAuthTenancyUnique (email/slug/token_hash/auth_subject),
  TestUpgradeFromV1 (data preservation across reopen + bootstrap)

Mashūra-reviewed: added UNIQUE on token_hash, composite FK on
join_tokens.created_by, partial unique on auth_subject, ON DELETE clauses,
FK indexes, fixed bootstrap to plain INSERT, expanded test coverage.

Verified: go build, go test -race, go vet, gofmt, buf lint, buf breaking —
all clean.

Trello: https://trello.com/c/Ba4YYGXM/148

* feat(card148): P4b — SO_PEERCRED local auth + fail-closed TCP

P4b implements Unix-socket peer-credential authentication (SO_PEERCRED) and
closes the TCP authentication bypass that existed since P3.

## What changed

### SO_PEERCRED + principal resolution
- New internal/auth/peercred package: platform-conditional extraction of
  Unix-socket peer credentials. Linux uses GetsockoptUcred (SO_PEERCRED);
  other platforms return unsupported (fail-closed).
- New internal/auth package: PrincipalResolver interface + LocalResolver
  that maps the daemon owner UID (os.Geteuid) to the seeded local owner
  principal (tnt_local/usr_local/owner/AuthLocal). Rejects all other UIDs
  including root. Fail-closed: no credentials -> ErrUnauthenticated.
- http.Server.ConnContext hook captures peer creds at connection-accept
  time and stores them in the request context. The resolver reads them
  per-request.
- All 11 Connect handler methods (8 SessionHandler + 3 EventHandler) now
  call resolvePrincipal(ctx, resolver) instead of the old localPrincipal()
  stub. SystemHandler (Health/GetServerInfo) remains intentionally
  unauthenticated — it exposes no session or tenant data.

### TCP security hole closed
- TCP listener now serves ONLY static files (webStaticHandler). Connect
  RPC handlers are NOT mounted on TCP. Before P4b, the TCP path served
  Connect RPC with EmbeddedPrincipal (owner) — an authentication bypass.
- HandlerWithStatic (Connect + static on one mux) deleted — it was a
  loaded footgun that could reopen the hole.
- serve() fixed: TCP server errors are logged, not returned to the
  daemon main loop (only Unix-socket errors stop the daemon).

### Store tenant predicates
- EventLog interface unchanged (Read/LastSeq have no tenant_id parameter).
  The service-layer lookup() is the tenant isolation gate, as documented
  in the EventLog contract. Cross-tenant tests (cross_tenant_test.go)
  verify all read/write paths reject cross-tenant access with
  ErrSessionNotFound. Mashura confirmed this is the correct stance;
  adding SQL predicates without a tenant input would be security theater.

### Error handling
- resolvePrincipal distinguishes auth.ErrUnauthenticated (CodeUnauthenticated)
  from other resolver errors (CodeInternal via mapError fallthrough).
  This prepares the seam for P4c DB-backed resolvers.
- errUnauthenticated added to errors.go map.
- Nil resolver guard: NewServer panics at construction, not at first request.
- ConnContext logs peercred extraction failures (fail-closed but visible).

## Mashura review
- Plan reviewed by 3 panels (gpt-5.6-sol, claude-fable-5, glm-5.2).
- Implementation reviewed by 3 panels.
- Key feedback folded in:
  * TCP security hole (critical blocker) — Connect removed from TCP
  * os.Geteuid() not os.Getuid() (real vs effective UID)
  * resolvePrincipal error distinction (auth vs internal)
  * HandlerWithStatic deletion (loaded footgun)
  * Stale comment fixes (listenUnix, tcpSrv field, serve(), role-refresh)
  * Embedded resolver kept in production package with explicit test-only
    doc (Go internal package visibility requires this for cmd/wakild tests)

## Tests
- internal/auth/peercred: Unix-socket + TCP extraction tests
- internal/auth: LocalResolver tests (owner UID, wrong UID, no creds, root)
- cmd/wakild/p4b_test: TCP does NOT serve Connect RPC, static files still
  served, Unix socket RPC works
- cmd/wakild/p4b_integration_test: real SO_PEERCRED path (success on Linux,
  UID mismatch rejection, no-credentials rejection over TCP)
- Existing cross-tenant tests (cross_tenant_test.go) verify service-layer
  tenant isolation for all read/write paths
- Web integration tests updated: RPCs now go through Unix socket, TCP
  serves only static files

Verified: go build ./..., go test -race (cmd/wakild, server/connect, auth,
sessionhost), go vet, gofmt — all clean.

Refs: Trello card #148 (https://trello.com/c/Ba4YYGXM/148)

* P4c: join token system + session cookies

Implements the join token onboarding flow and browser session cookies
for the wakild daemon. Admin (local owner via SO_PEERCRED) issues
one-time-use join tokens; clients exchange them for opaque server-side
session cookies stored in SQLite.

New packages:
- internal/auth/tokenstore: DB queries for join_tokens + web_sessions
- internal/auth/jointoken: token generation (256-bit CSPRNG, jnt_ prefix),
  issuance with role-based authorization, atomic one-transaction exchange
- internal/auth/tokenresolver: WebSessionResolver (cookie -> principal),
  distinguishes ErrCredentialAbsent (try next) from ErrInvalidCredential
  (hard fail, no fallthrough)

New proto:
- auth.proto + auth_service.proto: AuthService with CreateJoinToken,
  ListJoinTokens, RevokeJoinToken, ExchangeJoinToken (public),
  WhoAmI, Logout. No Login/Refresh (OIDC/P4e scope).

Migration 003:
- web_sessions table (opaque token, SHA-256 hashed, sliding + absolute
  expiry, FK to memberships ON DELETE CASCADE)
- join_tokens recreation: fix ON DELETE SET NULL -> CASCADE (security:
  SET NULL converts bound tokens to create-user-on-exchange tokens),
  add revoked_at column

Security (Mashura-reviewed, 3 panels):
- 256-bit CSPRNG token entropy, SHA-256 hash at rest
- One-time-use via conditional UPDATE (used_at IS NULL AND expires_at > now
  AND revoked_at IS NULL) in a single transaction
- auth_subject NEVER from exchange request (OIDC only, P4e)
- Only owners can issue owner-role tokens
- Cookie: HttpOnly, SameSite=Strict, Path=/
- Origin validation middleware (CSRF defense-in-depth)
- Generic errors (no token enumeration: expired vs used vs revoked)
- Server-side sessions: immediate revocation, current role read from
  memberships at resolve time (not cached in session row)

Daemon wiring:
- TCP server now mounts Conne…
@dependabot @github

dependabot Bot commented on behalf of github Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Dependabot tried to update this pull request, but something went wrong. We're looking into it, but in the meantime you can retry the update by commenting @dependabot recreate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants