Skip to content

Release 1.11.0 (Release C): GIT-86, 98, 99, 101, 102, 73, 105, 100, 107, 109, 83, 112 - #48

Merged
nTEG-dev merged 35 commits into
mainfrom
release/C
Sep 22, 2026
Merged

nTEG-dev merged 35 commits into
mainfrom
release/C

Conversation

@nTEG-dev

@nTEG-dev nTEG-dev commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

What

Release 1.11.0 (Release C): 11 PRs stacked on origin/main (b1b3cb9, 1.10.0), then 7a3ac5c bumping 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.

PR Ticket
#39 GIT-99, GIT-112 hooks resolve .gitmem like the server; Stop enforcement without jq; payload path named
#37 GIT-86 never resume a session of another project
#38 GIT-98 per-row delta sync of the embedding index
#40 GIT-101 every write tool reports durable / stored_in
#41 GIT-102 bounded write-path check; self-describing close WARNs
#42 GIT-73 metrics wait for their session row
#44 GIT-105 thread triples reference the thread row id
#43 GIT-100 migrate-root merges memory files by record id
#45 GIT-107 roots compared by real path
#46 GIT-109 static schema-drift gate against v1.8.0
#47 GIT-83 release gate; publish depends on the venue and hook jobs

Conflict resolutions

All were of the kinds agreed in advance. Nothing else came up.

  1. Test mocks missing 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-supabase and session-close-bridge. Each gets findSessionById → null (abb772a). Nothing failed before the fix, because no test reaches that fallback.
  2. 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.
  3. session-close.ts sessionRowStored:
  4. tsc, a return without 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 predates WriteResult. 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)

Gate Result
tsc --noEmit ✅ clean, run after every merge
Unit (vitest run) ✅ 1357 / 1357 (94 files)
Hooks suite, emulated slim PATH (no jq, no ps, env -i) ✅ 46 / 46
Venue vujndhcumgroztfkkdus reset to v1.8.0:schema/setup.sql → flow ✅ exit 0: close persisted, relevance readable, threads 3 = 3, resolve durable, no-payload close names the path, self-describing WARNs, health write-path verdict, 3 thread triples linked, no false stranded notice
→ egress --rows 250 --usage 5 ✅ exit 0; no full re-download after the cold start (921,750 B cold, then 100 B / 42,823 B)
→ projects ✅ exit 0 (11/11 cross-project checks)
current setup.sql applied twice → flow ✅ both applications clean; exit 0
EXPECTED_FAILURES ✅ empty; 0 known kinds seen in all four runs. The only non-2xx is the read-only capability probe for task_observations, which the driver treats as expected.
Drift analyzer ✅ the same 11 known findings (3 GIT-109 probe-guarded, 8 GIT-106 dev tier), nothing new; 77 call sites, 384 accesses, 5 known unresolved

The venue was restored to v1.8.0 afterwards. Production (cjptxyezuxdiinufgrrm) was never touched.

CI gate on GitHub

✅ Run 35687261565 (workflow_dispatch on release/C at f3fdae3): build (18, 20, 22) ✅, hooks-without-jq ✅, venue-gate ✅. Every venue step ran, none were skipped: the v1.8.0 reset via psql, then flow, egress and projects, then the current setup.sql applied 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

Claude and others added 30 commits September 21, 2026 20:02
… (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().
Conflicts:
- session-close.ts: #40 and #41 add the same sessionRowStored flag; one
  declaration kept (one assignment after the upsert), comment covers both uses.
- blank-supabase.mjs: all result fields and all checks kept.
tsc clean; no new returns to type.
Conflicts (blank-supabase.mjs):
- EXPECTED_FAILURES: #42 removed GIT-73 and #44 removed GIT-105; both
  removals kept, so the list is empty.
- all result fields and all checks kept.
tsc clean.
Claude and others added 5 commits September 21, 2026 21:16
Conflicts (blank-supabase.mjs): all result fields and all checks kept.
migrate-root.ts auto-merged (#43's merge + #45's sameDirectory). tsc clean.
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
nTEG-dev marked this pull request as ready for review September 22, 2026 04:36
@nTEG-dev nTEG-dev changed the title Release C (unversioned): GIT-86, 98, 99, 101, 102, 73, 105, 100, 107, 109, 83, 112 Release 1.11.0 (Release C): GIT-86, 98, 99, 101, 102, 73, 105, 100, 107, 109, 83, 112 Sep 22, 2026
@nTEG-dev
nTEG-dev merged commit f93c3a3 into main Sep 22, 2026
6 checks passed
@nTEG-dev
nTEG-dev deleted the release/C branch September 23, 2026 05:49
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.

1 participant