Repository navigation
Merged
Conversation
… (GIT-86) One desktop process serves every chat, so hostname+pid does not name a session. session_start(Y) found the open X session by hostname+pid, replaced the caller's project with X's (stderr only) and carried on in X. - findSessionByHostPid and findResumableSessionOnDisk take the requested project and only return a session of that project. - params.project set and no match: a new session. The registry slot is now hostname+pid+project, so registerSession no longer displaces X, X is not marked superseded, and pruneOrphanedDirs no longer sees X as unregistered. - params.project absent: the most recent session is resumed, and the display names its project (project_from_resumed_session). - force:true carries startedAt, surfaced scars, confirmations, observations and children forward only from a session of the same project. Confirmations were never carried before, and the free tier dropped the carried startedAt. - session_close's registry fallback looks the session up by id. +9 unit tests (4 registry, 5 through session_start); 8 fail on origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…GIT-86)
session_start(X), session_start(Y), session_start(X), session_start({}),
session_start(X, force) through one server over stdio, as the desktop app
does. Checks the stored rows, close_compliance, the registry and the session
directories. Exits 1 on 1.10.0 (Y resumes X); exits 0 with the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…isses (GIT-98) On a Pro store the fingerprint misses on nearly every session_start: refresh_scar_behavioral_scores() rewrites each scar with >=3 recent usage rows and bumps its updated_at. Each miss re-downloaded the whole index, every row with its 1536-dim embedding. - A miss with a cache from this store downloads the manifest (id, updated_at; no vectors) with the bulk load's filters, order and limit. - Rows whose id and updated_at match are reused from disk; changed and new ids are fetched in full, 100 ids per request; rows gone from the manifest are dropped. The result is in manifest order, what a full download returns. - Any failure, an empty manifest, or no cache from this store: the full download, as before. cache-flush still always takes the full download. - The bulk load, fingerprint, manifest and delta fetch share one filter set. Venue (v1.8.0 setup.sql, 250 learnings, --usage 5): the start after a session_start drops from 921,780 B to 42,823 B — 245 reused, 5 fetched. +8 unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rt (GIT-98) Exits 1 on 1.10.0 (the start after each session_start re-downloads ~922 KB); exits 0 with per-row delta sync. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rint the payload path (GIT-99) Since GIT-91 the server resolves its root independently of cwd, but the hooks still read ".gitmem/..." relative to theirs. A hook running in a repo read a different store from the one the server writes: no session, or someone else's, and a closing-payload.json the server never reads. - hooks/scripts/resolve-root.sh: GITMEM_DIR, then GITMEM_HOME/.gitmem, then ~/.gitmem, made absolute. Sourced by recall-check, post-tool-use and session-close-check. - Registry entries on this host whose pid is dead are skipped; entries without a pid or from another host are kept. recall-check takes the newest live session instead of sessions[0]. - The Stop hook prints the resolved absolute payload path, JSON-escaped. - Fixes a pre-existing break: without jq, session-close-check's node fallback was brace-expanded by bash into two broken node calls, so the Stop hook never blocked (3 of 32 hook tests red on main on a jq-less machine). test-hooks.sh: the existing tests relied on cwd resolution (cd $TMPDIR finding $TMPDIR/.gitmem); they now export GITMEM_DIR to the same store. +12 tests (44/44; main 31/44). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ose with (GIT-99) A standard close with no closing-payload.json and no inline reflection now returns "closing-payload.json not found at <absolute path>" instead of the "requires task_completion / closing_reflection with N answers" family, which sent agents to rewrite answers they had already written to the wrong root. An unparseable payload is reported as found but unreadable, at its path. session-close-supabase.test.ts: three title/decision tests closed "standard" with neither payload nor reflection, reaching persistence only because validation is mocked. That input is now rejected up front, so they carry a minimal reflection to keep testing what they test. +5 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ad path (GIT-99)
Exits 1 on 1.10.0 ("requires closing_reflection with 7 answers"); exits 0
with the fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hread stops claiming success (GIT-101)
resolve_thread computed supabaseSynced, logged it to metrics and returned
success: true regardless, so a resolve the durable store never saw looked
done while other sessions still saw the thread open.
- WriteResult { success, durable, stored_in } (types + services/write-result.ts)
is create_thread's contract, now shared: success = durable || !hasSupabase(),
and false whenever nothing was stored.
- Applied to create_learning, create_decision, create_thread, resolve_thread,
archive_learning, record_scar_usage, record_scar_usage_batch, session_close,
promote_suggestion (passes create_thread's outcome through) and
save_transcript. Result types extend WriteResult, so every return states it.
- resolve_thread on pro without a durable write: success false,
stored_in local_only, and a display that says RESOLVED LOCALLY ONLY.
- session_close reports whether the session row reached the store on every
exit, including a failure after the row landed.
One parameterized test, Supabase failing at the network, pro tier: each of the
10 tools reports success false, durable false, the right stored_in, and a
display that says so. 10 of 12 fail on origin/main (resolve_thread: success
true). +12 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eads (GIT-101) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…elf-describing close WARNs (GIT-102) - checkWritePathWithTimeout() races the startup probe against 10 s. A store that never answers gives "timed out after 10 s, durability UNVERIFIED" instead of silence; a late answer replaces it, marked late. server.ts uses it on both startup paths. - The last verdict is exposed through health (write_path, and a first line "Write path: ..."). It was visible only on stderr, and not at all on a hang. - A probe that errored with anything but a missing table was ignored, so a store refusing every connection logged "Write-path OK". It is now "unreachable", durability UNVERIFIED. - session_close's "WARN N write failures" becomes one line per failing subsystem: name, failure count, cause (its latest error), and a mandatory "session content stored OK / NOT stored" clause, from whether the session row actually landed. +14 tests; the unreachable and timeout cases fail on origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h verdict in health (GIT-102)
Exits 1 on 1.10.0 ("WARN 1 write failure"; health without a verdict).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (GIT-73) session_start created its session row fire-and-forget and recorded its metrics at the same moment. gitmem_query_metrics.session_id references gitmem_sessions(id), so whichever request reached the store first decided whether the metrics insert failed with 409 (FK). recall right after session_start raced the same way. - session_start registers the pending session insert; recordMetrics waits for it (bounded at 15 s) before writing a row that references that session. - If the session row did not land, the metrics row is written without the FK and keeps the id in metadata.session_id, where session_close already reads it (GIT-109), instead of being lost. - Sessions this process did not create (resumed, another process's) are written immediately, as before. +5 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A 409 from gitmem_query_metrics now fails the run. 10 flow runs on the v1.8.0 schema with the fix: 0 409s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g them (GIT-100) learnings.json, threads.json, decisions.json and sessions.json were skipped as "already exists", so a destination holding any local memory received none of the project store's, and the report listed the user's scars under "skipped". - Those four are merged by record id: new ids are added; where both sides hold a record the newer wins (updated_at, else resolved_at, last_touched_at, created_at), the destination on a tie or when neither is dated. - Every loser is written to <dest>/migrate-root-conflicts.json with which side was kept, deduplicated so a re-run logs nothing new. - The destination's memory files are backed up to <dest>/backups/ before the first write; nothing is backed up when nothing changes. - Idempotent: a re-run changes nothing. - A memory file that is not a readable list of records is reported as NOT merged (exit 1) and left untouched; never listed as skipped. - Other files keep copy-if-absent; backups/ and the conflicts log are never migrated out of a source. migrate-root.test.ts: "never overwrites a file that already exists" and "reports why each file was skipped" used learnings.json and asserted it was skipped, i.e. the old behaviour. They now use config.json to keep testing the non-memory rule. +11 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…IT-105)
knowledge_triples.source_id is a UUID column on every schema, including the
v1.8.0 setup.sql. Thread triples wrote the thread id ("t-xxxxxxxx") into it,
so every create_thread / resolve_thread triple failed with 400 (22P02) and
the failure was swallowed. No schema change needed:
- source_id is the thread's gitmem_threads.id, a UUID the store already has.
create_thread passes the row id createThreadInSupabase returned;
resolution triples look it up by thread_id. A failed or empty lookup writes
the triple with source_id null rather than losing it.
- writeTriples never sends a non-UUID source_id from any caller; it logs and
sends null instead.
thread-triples.test.ts: two assertions expected source_id "t-abc12345", the
value the column rejects. They now expect the row id. +5 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…expected failures (GIT-105) Exits 1 on 1.10.0 (0 thread triples stored); exits 0 with the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
findStrandedProjectRoots compared candidate and home as strings. On macOS /var is a symlink to /private/var: process.cwd() reports /private/var/... and a home or GITMEM_DIR under $TMPDIR is spelled /var/..., so session_start showed "Memory store found but NOT being read" about the store it was reading. Any symlink in either path did the same. - canonicalPath() resolves with fs.realpathSync.native; a path that does not exist yet resolves through its nearest existing ancestor. - A candidate is not stranded if it is the same directory as the home root or an explicit GITMEM_DIR (the root actually being read). - migrate-root's "source and destination are the same" check uses it too. +5 tests (symlinked home, a different store still reported, GITMEM_DIR, sameDirectory spellings, and the macOS /var case); 4 fail on origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The driver's store lives under $TMPDIR, so 1.10.0 shows the notice on every macOS run (exit 1); exits 0 with the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…step 2) Customers never run SQL after initial setup, so every table and column gitmem touches must exist in the v1.8.0 schema or be deliberately guarded. - tests/helpers/schema-drift.ts parses setup.sql into table -> columns (CREATE TABLE, ALTER ... ADD COLUMN, views) and walks src/ with the TypeScript compiler API. It collects table + payload keys reaching directUpsert / directPatch / upsertRecord / storage.upsert, and select / filter / order keys reaching directQuery / directQueryAll / listRecords / getRecord. It follows getTableName(), const indirection, spreads, conditional spreads, later x.prop = assignments, local functions' returns and .map/.filter element shapes. Free-tier-only code is set aside; anything it cannot resolve is reported, never assumed fine. - tests/fixtures/schema/setup-v1.8.0.sql is the floor, byte-identical to `git show v1.8.0:schema/setup.sql`. - schema-drift.test.ts fails on a finding not allowlisted by ticket, a stale allowlist entry, an entry without a GIT-### ticket, a new unresolved call site, or a SESSION_COLUMNS entry v1.8.0 lacks that is not production-only. A synthetic source tree with planted drift proves the analyzer still catches it. First run: 74 call sites, 362 column accesses, 11 findings — 3 probe-guarded production-only columns (GIT-109) and 8 variant tables/columns reachable only on the dev tier (GIT-106). No other drift. +9 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
resolve-root.sh decided liveness with `ps -p`. On an image without ps (e.g. node:*-slim, the natural jq-less CI runner) that call fails, so every registry entry looked dead and the Stop hook never blocked — the GIT-112 failure again, through a different missing tool. gitmem_pid_alive uses the kill -0 builtin, with EPERM counted as alive. +2 hook tests (46/46): the check with no ps on PATH, and an EPERM pid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ok tests without jq (GIT-83, GIT-112) - venue-gate (v* tags and workflow_dispatch): puts the disposable venue on `git show v1.8.0:schema/setup.sql`, runs the driver (flow, egress --usage 5, projects when the driver has it), applies the current setup.sql twice on top, and runs flow again. Missing secrets fail the job; a concurrency group keeps two runs from wiping each other's venue data. Driver reports are uploaded (netlogs hold method/path/status/size only). - scripts/venue-schema.sh: reset-to / apply via psql, one transaction, ON_ERROR_STOP, PostgREST schema reload. Refuses production (by ref and by URL) and any DB URL that does not name VENUE_REF, before connecting. - tests/e2e/venue/reset-gitmem-objects.sql: drops only what setup.sql creates. - hooks-without-jq (every run): hooks/tests/test-hooks.sh in node:20-bookworm-slim, asserting neither jq nor ps is installed (GIT-112). - publish needs build, hooks-without-jq and venue-gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
) GIT-86 (#37) made session_close's registry fallback look the session up by id. Four mocks of active-sessions.js exported only findSessionByHostPid — session-close-validation (named in the stack plan), session-close-payload-path (#39, whose mock header was copied from it), session-close-supabase and session-close-bridge. No test reached that fallback, so nothing failed; any future one would have thrown on the missing export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # tests/e2e/blank-supabase.mjs
Conflicts: - session-close.ts: #39's payloadReadError and #40's sessionRowStored are separate declarations; both kept. - blank-supabase.mjs: #40's resolve step before #39's no-payload close (both before the real close); both result fields; all three checks. Semantic (tsc): #39's "closing-payload.json not found" return predates WriteResult; it is a pre-write exit, so it gets ...notStored().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CHANGELOG [Unreleased] -> [1.11.0] - 2026-09-21, fresh [Unreleased] above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nTEG-dev
marked this pull request as ready for review
September 22, 2026 04:36
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
Release 1.11.0 (Release C): 11 PRs stacked on
origin/main(b1b3cb9, 1.10.0), then7a3ac5cbumping the version to 1.11.0. CHANGELOG[1.11.0] - 2026-09-21, with a fresh[Unreleased]above it. Not merged, tagged or published yet. Phase 2 waits for Chris's go.Merge order: #39, #37, #38, #40, #41, #42, #44, #43, #45, #46, #47.
.gitmemlike the server; Stop enforcement without jq; payload path nameddurable/stored_inmigrate-rootmerges memory files by record idConflict resolutions
All were of the kinds agreed in advance. Nothing else came up.
findSessionById(fix: never resume a session of another project (GIT-86) #37), in 4 files, not just the one expected:session-close-validation,session-close-payload-path(fix: hooks resolve .gitmem like the server; session_close names the payload path (GIT-99) #39 copied its mock header from it),session-close-supabaseandsession-close-bridge. Each getsfindSessionById → null(abb772a). Nothing failed before the fix, because no test reaches that fallback.blank-supabase.mjs: conflicts at fix: sync only changed rows on a learnings-fingerprint miss (GIT-98) #38, fix: every write tool reports durability; resolve_thread stops claiming success (GIT-101) #40, fix: bounded write-path check with an UNVERIFIED verdict; self-describing close WARNs (GIT-102) #41, fix: thread triples reference the thread row id, not its t- id (GIT-105) #44 and fix: compare .gitmem roots by real path, not string (GIT-107) #45. Every mode, step, result field and check is kept. Order: fix: every write tool reports durability; resolve_thread stops claiming success (GIT-101) #40's resolve step, then fix: hooks resolve .gitmem like the server; session_close names the payload path (GIT-99) #39's no-payload close, then the real close.EXPECTED_FAILURES: fix: metrics wait for their session row instead of racing it to a 409 (GIT-73) #42 removed GIT-73 and fix: thread triples reference the thread row id, not its t- id (GIT-105) #44 removed GIT-105; both removals are kept, so the list is empty.session-close.tssessionRowStored:payloadReadError(separate declarations);durable/stored_in, at fix: every write tool reports durability; resolve_thread stops claiming success (GIT-101) #40: fix: hooks resolve .gitmem like the server; session_close names the payload path (GIT-99) #39's "closing-payload.json not found" return predatesWriteResult. It's a pre-write exit, so it gets...notStored(). That was the only one; tsc was clean after every other merge.Gates (on this branch)
tsc --noEmitvitest run)PATH(no jq, no ps,env -i)vujndhcumgroztfkkdusreset tov1.8.0:schema/setup.sql→flowhealthwrite-path verdict, 3 thread triples linked, no false stranded noticeegress --rows 250 --usage 5projectssetup.sqlapplied twice →flowEXPECTED_FAILUREStask_observations, which the driver treats as expected.The venue was restored to v1.8.0 afterwards. Production (
cjptxyezuxdiinufgrrm) was never touched.CI gate on GitHub
✅ Run 35687261565 (
workflow_dispatchonrelease/Catf3fdae3): build (18, 20, 22) ✅, hooks-without-jq ✅, venue-gate ✅. Every venue step ran, none were skipped: the v1.8.0 reset viapsql, then flow, egress and projects, then the currentsetup.sqlapplied twice, then flow. publish was skipped, as it should be (not a tag).Release
Version 1.11.0. Phase 2, after Chris says go: preflight, merge commit, main's CI, tag
v1.11.0, tag CI through both gates and publish, registry verification, GitHub Release.🤖 Generated with Claude Code