mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 21:19:18 +00:00
* feat(worker-pool): shared sliding pool + bounded semaphore + PGLite-clamp wrapper T1 + T2 of the v0.41.16.0 workers cathedral. New src/core/worker-pool.ts is the canonical primitive every --workers N bulk command in this wave (and future bulk commands) builds on. Atomic-claim invariant enforced by scripts/check-worker-pool-atomicity.sh (wired into bun run verify). BudgetExhausted bypass + AbortSignal composition baked into the helper so budget caps are a structural ceiling under concurrency, not a per-caller convention. The new resolveWorkersWithClamp wrapper composes existing autoConcurrency with PGLite-clamp + per-(command, requested) stderr dedup. Deliberately NOT a modification to shared autoConcurrency (silent today, used by sync + import); embed.ts keeps GBRAIN_EMBED_CONCURRENCY || 20 default per codex #13. 23 + 12 + 9 = 44 hermetic tests pin every contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: structural + dim-check regression suites for v0.41.16.0 wave - test/embed-helper-migration.test.ts (T3): asserts embed.ts's two sliding-pool sites are migrated to runSlidingPool, pre-migration shapes (let nextIdx = 0, Promise.all(Array.from(...))) are gone, GBRAIN_EMBED_CONCURRENCY || 20 default preserved, failureLabel threads page.slug. Per codex #16/#17 these are invariant assertions, not byte-equality on progress event ORDERING. - test/embedding-dim-check-facts.test.ts (T6): readFactsEmbeddingDim covers vector(N) + halfvec(N), halfvec-before-vector regex ordering pinned (codex #19), buildFactsAlterRecipe emits DROP INDEX + ALTER USING + CREATE INDEX (codex #18, not bare REINDEX), FactsEmbeddingDimMismatchError tagged class shape, assertFactsEmbeddingDimMatchesConfig PGLite skip + Postgres absent- column skip, doctor check + insert-cast wiring assertions. - test/extract-conversation-facts-workers.test.ts (T5): helper exports (extractConversationFactsLockId, PER_PAGE_LOCK_TTL_MINUTES), structural wiring (runSlidingPool, resolveWorkersWithClamp, withRefreshingLock, LockUnavailableError, delete-orphans-first before segment loop, preflight before pool, exit 3 when lock_skipped > 0), Minion handler round-trip. - test/extract-workers.test.ts (T7): --workers wiring on all 3 inner fs-walk loops (extractForSlugs, extractLinksFromDir, extractTimelineFromDir) + CLI parse + opts threading through runExtractCore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rebump v0.41.16.0 → v0.41.17.0 (queue collision with PR #1510) PR #1510 (garrytan/dynamic-regex-conversation-formats) claimed v0.41.16.0 on master in parallel. Advancing this wave to v0.41.17.0 so both can land cleanly. Pure mechanical version bump: - VERSION + package.json → 0.41.17.0 - CHANGELOG.md header + "To take advantage of v0.41.17.0" block - TODOS.md section header + v0.41.18+ forward references - CLAUDE.md inline version tags - Regenerated llms-full.txt / llms.txt No code changes. The actual workers cathedral feature set is unchanged from the two prior commits in this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): search-image-column probes column dim at runtime CI shard 5 failed on `searchVector column routing (v0.27.1)` with: error: expected 1280 dimensions, not 1536 The test had a hardcoded `fakeText1536` helper that seeded chunks at 1536-d vectors. Master's default embedding model switched from OpenAI text-embedding-3-large (1536) to ZeroEntropy zembed-1 (1280) so a fresh PGLite brain on CI now sizes content_chunks.embedding at 1280; the test's 1536-d INSERT trips pgvector's CheckExpectedDim. Fix: probe `content_chunks.embedding` width via `readContentChunksEmbeddingDim(engine)` in `beforeAll`, store in `TEXT_DIM`, and build `fakeTextDefault(seed)` at that width. The test now passes regardless of which default ships (the model has flipped twice and may flip again). Local dev (1536 from older config) and CI fresh-install (1280 from new default) both pass. Image-side vectors stay at 1024 (matches Voyage multimodal-3 + the column's fixed width on the image side). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): bump PGLite hook timeout for shard-4 deep-process files facts-anti-loop.test.ts and ingest-capture.test.ts were timing out in CI shard 4 with "beforeEach/afterEach hook timed out" after the v0.41.16.0 master merge brought migration count to 99. When these files run deep in a shard process that has already created ~20 PGLite engines, the WASM cold-start + 95-migration replay legitimately exceeds bun's 5s default hook timeout (observed 5.6s and 7.3s locally when reproducing). Bun's --timeout=60000 from scripts/test-shard.sh covers TEST timeouts but NOT hook timeouts; those default to 5s and must be set per-hook via the optional 2nd arg to beforeAll/afterAll. Reproduced locally by running the first 21 shard-4 files via head -21 /tmp/shard4-list.txt | xargs bun test → 179 pass, 2 fail (both with hook-timeout error) After fix: → 198 pass, 0 fail (the 4 anti-loop + 15 ingest-capture tests recover) Full shard 4 with fix: 955 pass, 0 fail. Full shard 5 with fix: 1261 pass, 0 fail. Also added a defensive diagnostic to the two put_page tests: if facts_backstop is missing in the response payload, throw with the full payload + isError so future failures surface the actual handler error instead of a bare "expected {...} got undefined" assertion. No-op when the test passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
4.0 KiB
Bash
Executable File
96 lines
4.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# CI guard: protect the worker-pool atomicity invariant (v0.41.15.0, D5).
|
|
#
|
|
# `src/core/worker-pool.ts:runSlidingPool` rests on `const idx = nextIdx++`
|
|
# being atomic across N concurrent workers. Two failure modes silently
|
|
# break the invariant; this guard rejects both.
|
|
#
|
|
# FAILURE MODE 1: `worker_threads` import in any file that imports
|
|
# `runSlidingPool` or `runWithLimit`. Pool work crossing kernel threads
|
|
# loses the JS event-loop guarantee. Two workers could claim the same
|
|
# idx; silent duplicate work, duplicate DB writes. Same failure shape as
|
|
# the per-page lock in extract-conversation-facts exists to defend
|
|
# against, but the lock is defense-in-depth — atomicity is the primary
|
|
# correctness story.
|
|
#
|
|
# FAILURE MODE 2: An `await` between the read and write of `nextIdx` in
|
|
# `worker-pool.ts` itself. Pattern like `const idx = await getNextIdx()`
|
|
# introduces a yield window between read and write; another worker can
|
|
# run during the yield and claim the same idx.
|
|
#
|
|
# Usage: scripts/check-worker-pool-atomicity.sh
|
|
# Exit: 0 when invariants hold, 1 when a violation is found.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
cd "$ROOT"
|
|
|
|
POOL_FILE="src/core/worker-pool.ts"
|
|
|
|
if [ ! -f "$POOL_FILE" ]; then
|
|
echo "OK: $POOL_FILE not present yet — guard is no-op"
|
|
exit 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# FAILURE MODE 1: worker_threads alongside the helper.
|
|
# Find every src/ file that imports from the helper, then check whether
|
|
# any of them ALSO imports node:worker_threads / worker_threads.
|
|
# -----------------------------------------------------------------------
|
|
|
|
IMPORT_PATTERN="from ['\"][^'\"]*worker-pool[^'\"]*['\"]"
|
|
HELPER_CALLERS=$(grep -rlE "$IMPORT_PATTERN" src/ 2>/dev/null || true)
|
|
|
|
if [ -n "$HELPER_CALLERS" ]; then
|
|
WORKER_THREADS_VIOLATIONS=""
|
|
for caller in $HELPER_CALLERS; do
|
|
if grep -E "from ['\"](node:)?worker_threads['\"]" "$caller" >/dev/null 2>&1; then
|
|
WORKER_THREADS_VIOLATIONS="$WORKER_THREADS_VIOLATIONS$caller\n"
|
|
fi
|
|
done
|
|
if [ -n "$WORKER_THREADS_VIOLATIONS" ]; then
|
|
echo "ERROR: worker_threads imported in file(s) that also use runSlidingPool / runWithLimit:"
|
|
# shellcheck disable=SC2059
|
|
printf "$WORKER_THREADS_VIOLATIONS"
|
|
echo
|
|
echo " The sliding pool's atomicity invariant relies on the single"
|
|
echo " JS event loop. worker_threads crosses kernel threads; two"
|
|
echo " workers can claim the same idx; duplicate work + DB writes."
|
|
echo " See src/core/worker-pool.ts header for the full invariant."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------
|
|
# FAILURE MODE 2: await between nextIdx read and write inside the helper.
|
|
# The legal forms are:
|
|
# let nextIdx = 0;
|
|
# const idx = nextIdx++;
|
|
# Anything matching `await.*nextIdx` or `nextIdx.*await` in the helper
|
|
# body indicates a yield window between read and write.
|
|
# -----------------------------------------------------------------------
|
|
|
|
# Strip multi-line comments + single-line comments before checking, so
|
|
# `await` mentions in documentation don't false-fire. The pool file's
|
|
# header explicitly mentions `await getNextIdx()` as the BAD pattern;
|
|
# without comment-stripping, this guard would always fail.
|
|
STRIPPED=$(sed -E '
|
|
# Drop /** ... */ block comments (greedy single-line form only).
|
|
/^\s*\/\*/,/\*\//d
|
|
# Drop // line comments.
|
|
s|//.*$||
|
|
' "$POOL_FILE")
|
|
|
|
if echo "$STRIPPED" | grep -E '(await\s+[a-zA-Z_$]*[Nn]ext[Ii]dx|nextIdx[^+]*await)' >/dev/null 2>&1; then
|
|
echo "ERROR: found await near nextIdx in $POOL_FILE"
|
|
echo " The claim `const idx = nextIdx++` must remain a single"
|
|
echo " synchronous statement. Inserting an await between the read"
|
|
echo " and write breaks atomicity: another worker can run during"
|
|
echo " the yield window and claim the same idx."
|
|
echo " See src/core/worker-pool.ts header for the full invariant."
|
|
exit 1
|
|
fi
|
|
|
|
echo "OK: worker-pool atomicity invariant intact"
|