feat(go-core): M1 session and message store over the existing SQLite - #1133
Merged
Conversation
Identical to the documents on docs/m1-go-session-store-spec, so the implementation subagents can read their briefs from this checkout. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…path rules Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ions in tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ainst a TS fixture Adds internal/ident, mirroring packages/teamcode/src/id/id.ts byte for byte: 6 big-endian bytes of ms*0x1000 + counter, bitwise NOT for descending ids, 14 base62 characters. script/gen-id-fixture.ts drives the real TS create/timestamp functions to produce go-core/internal/ident/testdata/ids-from-ts.json, which the Go tests check ordering and timestamp decoding against. The fixture generator's base timestamp is a small millisecond value (1_000_000_000) rather than a present-day one. The TS encoding packs ms*0x1000 into 6 bytes (48 bits), so any ms value at or above 2^36 (~1972-03-06) overflows and timestamp() no longer round-trips to the original ms — reproducible against the unmodified TS source with Date.now(). Both sides still sort correctly for values sharing the same 2^36 band, which covers all realistic near-term comparisons, but an exact-equality fixture needs a base under that ceiling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fixture generator restores base = Date.UTC(2026, 8, 9, 12, 0, 0) so the truncation the TS scheme actually exhibits at real timestamps is exercised, instead of being sidestepped with a base date small enough to avoid the 48-bit overflow. Each ascending row now stores both the raw ms passed to create() and the TS-computed decoded value (timestamp(row.id), null for descending rows). ident_test.go: TestTimestampMatchesTS compares Timestamp(row.ID) against row.Decoded rather than row.Timestamp. Ordering assertions are untouched, since they never call Timestamp() and only compare the raw stored ms values, which is valid because every fixture id falls inside one 2^36 ms wrap band. Adds TestTimestampTruncatesLikeTS, verified against the real, unmodified TS source first, asserting Timestamp(NewAt(...)) == ms & ((1<<36)-1). TestGoIdsInterleaveWithTSIds also calls Timestamp() on fixture and freshly generated ids and previously compared the result against the untruncated mid.Timestamp+1; with real 2026 timestamps this comparison mixes a truncated value with an untruncated one and fails on every run. Fixed by comparing against (mid.Timestamp+1) truncated the same way, which is what the ids actually decode to. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ixture TestTimestampMatchesTS and the ordering tests only covered ascending ids; nothing checked that a Go-generated descending id (used for session ids) produces the same 12 hex time bytes as the TS reference, and Timestamp() cannot decode descending ids at all, leaving the session-id path unguarded. Adds TestDescendingHexMatchesTS: for every fixture "ses" row (the first NewAt call issued for that millisecond in the generator, TS counter 1), calls NewAt(PrefixSession, true, row.Timestamp) and compares the 12 hex chars after the prefix against the fixture id's. Adds TestAscendingHexMatchesTS: for every fixture "msg" row (TS counter 2, since "ses" was generated first for that millisecond), calls NewAt(PrefixMessage, false, row.Timestamp) twice and compares the second call's hex chars against the fixture id's, reproducing the same counter progression TS used when generating the fixture. Both tests iterate the fixture in generation order and rely on consecutive rows always landing on a different millisecond so Go's package-level counter resets the same way TS's did; neither is marked parallel, and a shuffled, repeated (-shuffle=on -count=3) run confirms there is no hidden ordering dependency on the other tests in the file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…set pagination Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…scoped by message Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…known replay positions Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ded mode Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ull patch mapping Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds a harness that spawns the go-core binary against a real SQLite file, runs one scenario (session create/rename/fork, message and part upsert/removal, todo replace, session delete) through both the TS Session.Service/Todo.Service and the equivalent /v1/session HTTP calls, and diffs normalized session/message/part/todo table dumps. Skips instead of failing when GO_CORE_BINARY is unset so `bun turbo test:ci --filter=teamcode` keeps working without Go. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The parity test overrides TEAMCODE_DB with a real file path so go-core can open a database file the preload's in-memory default can't provide. Without restoring it afterward, the override leaked into every test file loaded later in the same Bun process. Save the pre-override value and restore it in an afterAll registered inside the describe block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ites Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… SQLite store Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fix all gofmt and golangci-lint findings in pre-existing go-core files (errcheck, staticcheck, unused) so the Task 13 CI job's Format and Lint steps pass. Behaviour is unchanged: unchecked errors are logged or discarded with `_ =` matching the existing convention in main.go, unused identifiers are removed, and the staticcheck rewrite applies De Morgan's law without altering the parsed result. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rebase onto develop resolved a conflict in main.go by taking the develop side of the file, which removed the registerV1Routes(mux) call added with the /v1/session handlers. golangci-lint flagged the function as unused; the call is back where the handlers commit placed it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
writeError built the body with a format string, so a message carrying a double quote produced a payload the client could not parse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A database migrated past the known floor could still miss a column the store reads, which surfaced as a query error per request instead of the degraded mode. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The insert copied time_created into time_updated, while the TypeScript projector lets the column default to the insert clock. The parity dump now ranks timestamps instead of dropping them, so the two runtimes are compared on ordering. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Subscribing to an unknown session allocated a ring that nothing ever freed, so a client polling arbitrary ids grew the log without limit. Deleting a session now drops its ring and closes its subscribers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The filter normalized to forward slashes, which never matched the backslash paths the TypeScript side stores on Windows, and a relative directory was compared unresolved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A wrong id prefix, a malformed cursor, a body that is not a JSON object, a null on a non-nullable patch path and a create against a missing project all surfaced as 500. The store now marks them as invalid input and the route layer maps that to 400. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The database handles stayed open after the HTTP server stopped, so the WAL checkpoint in Close never ran on exit. newV1State loses its error return, which was always nil. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An upsert of an existing id echoed the request path instead of the row, so a part re-sent under another message reported the wrong messageID while the database kept the original. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A write for a deleted session answered 200 with an "ignored" body that no client decodes, so the caller had to inspect the payload to learn nothing was stored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wire object kept a model shape that the TypeScript fromRow discards, so M2 would decode a model the TypeScript side never returns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rite OpenWithBusyTimeout becomes exported so the server test can lower the timeout through the openStore seam and observe 503 busy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The spec listed a fork body without directory and version, said nothing about the archived filter M2 has to pass, and CONTRACTS.md did not mention the /v1 surface at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… not by clock Ranking every timestamp compared millisecond ties between two runtimes that do not derive the values from the same input. The dump now compares message time_created exactly, part time_created by row order, the remaining time columns by presence plus time_updated >= time_created, and keeps data.time as stored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the runtime-assembled PRAGMA, WHERE, SET and IN fragments with literal statements bound by parameters: a table-to-query map for the schema probe, one all-flags list query, one statement per patchable column and json_each for the part hydration. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Break patchColumns into scalar, json, nested and token helpers, move the publisher and subscriber goroutine bodies of the event log concurrency test into named helpers, name the step-finish usage probe structs and drop a redundant binding in the v1 handler test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ture writer Replace the assertion-less skip placeholders with describe.skipIf so the shared-database override only runs when the suite runs, split the table dump into row and timestamp helpers, assert lengths with toHaveLength, and reject a fixture output path outside the repository root. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The instrumented read takes 21.5 s against a 5 s budget, so the budget test now skips under -short or the race build tag and CI enforces it in its own uninstrumented step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sonar reads any concatenation in query text as dynamic SQL, so the shared column list is written out inside selectSessionByIDQuery and listSessionsQuery and the sessionColumns constant is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What
Milestone M1 of the plan to read and continue Claude Code and Codex sessions from teamcode: go-core becomes able to own session, message, part and todo persistence on the same
opencode.dband drizzle schema the TypeScript side uses today, without changing TypeScript behaviour. Spec and plan are indocs/rewrite/(also on #1132).internal/store: opens the file with the TS pragmas and path rules (TEAMCODE_DB, channel, XDG), one serialized writer withBEGIN IMMEDIATE,busy_timeout, no WAL recovery on the Go side; schema check = migration floor (__drizzle_migrations) plusPRAGMA table_infocolumn shape, degraded mode when behind.internal/ident: TS id scheme (48-bitms*4096+counter, descending sessions, ascending messages/parts/events), pinned byte-for-byte against an 800-id fixture generated bypackages/teamcode/src/id/id.ts.internal/sessiondb: sessions, messages, parts, todos, fork; write rules copied from the TS projectors (usage deltas,time_updatednever bumped implicitly, null clears / absent keeps, clock guard inside the transaction, late writes swallowed); keyset pagination with the TS cursor.internal/eventlog: per-sessionseq, replay ring, lag signalling, no silent drops;GET /v1/eventsSSE withLast-Event-ID.cmd/server:/v1/session/*mirroringSession.Servicewith TS wire shapes; 400/404/503 mapping; legacy/session/*untouched.packages/teamcode/test/gocore/, skipped withoutGO_CORE_BINARY); 50k-message read budget (437 ms of 5 s).go-corejob (gofmt, vet, golangci-lint,go test -race, build, parity); legacy go-core code formatted and lint-clean so the job passes.PersistentStore; updatesARCHITECTURE.md,PERFORMANCE_PLAN.md,CONTRACTS.md.Review trail
Fifteen tasks, each implemented and reviewed by separate agents, then a whole-branch review and one fix wave (13 commits). Known follow-ups left open on purpose, none blocking M2:
v1.Close()inmain.goruns afterserver.Shutdown, butmainreturns as soon asServereturnsErrServerClosed, so the PASSIVE checkpoint at exit does not run (spec §3.1). WAL auto-checkpoints; the TS side still owns recovery.time_updatedfix.eventlog.Dropdoes not disconnect SSE clients of a deleted session; a publish racing the delete can recreate the ring.modeldropped on empty-string id where TS keeps it;PATCHwith a non-object body answers 500.Verification
go build ./... && go vet ./... && go test -count=1 ./...green;golangci-lint run ./...0 issueson LF content.bun x tsc --noEmitclean;bun test test/gocore/2 pass with the built binary, 5/5 consecutive parity runs;test/session/unchanged.-racewas not run locally (no C compiler on the dev box); this CI run is its first execution.🤖 Generated with Claude Code