Skip to content

feat: add Antigravity IDE support (hooks, plugin, skill, docs, tests) - #1633

Merged
igorls merged 6 commits into
MemPalace:developfrom
undeadindustries:feat/antigravity-support
Jun 10, 2026
Merged

feat: add Antigravity IDE support (hooks, plugin, skill, docs, tests)#1633
igorls merged 6 commits into
MemPalace:developfrom
undeadindustries:feat/antigravity-support

Conversation

@undeadindustries

Copy link
Copy Markdown
Contributor

Summary

Adds first-class integration with Google's Antigravity IDE as a third sibling to the existing Claude Code and Codex hook integrations.

This is strictly additive: no existing files in main are restructured. The integration scope is bounded by what Google's official Antigravity docs actually expose — the full audit (with verbatim quotes + URLs + dates + omitted-surface rationale) is in hooks/antigravity/INVESTIGATION.md.

What ships

Surface File / location
Plugin manifest .antigravity-plugin/plugin.json — verified minimal shape
MCP server .antigravity-plugin/mcp_config.json — registers mempalace-mcp stdio
Skill .antigravity-plugin/skills/mempalace/SKILL.md — real file (no symlinks)
Stop hook hooks/antigravity/mempal_save_hook_antigravity.sh — counter + auto-mine
PreInvocation hook hooks/antigravity/mempal_wake_hook_antigravity.sh — gated to invocationNum==1
Installer hooks/antigravity/install.sh — idempotent, __PLUGIN_DIR__ substitution, basename-guarded uninstall
Documentation website/guide/antigravity.md + sidebar entry in config.mts
Standalone examples examples/antigravity/{hooks.json,mcp_config.json,README.md}
Tests 56 new tests across 3 test files

What is deliberately NOT shipped (with reasons)

Documented in hooks/antigravity/INVESTIGATION.md:

  • PreCompact equivalent — no external Antigravity surface; only the in-process Python SDK has @hooks.on_compaction. The Stop hook still catches end-of-turn state; auto-compaction mid-turn loses some content but the verbatim transcript ingestion covers long-term recall.
  • Slash-commands / commands/ — Antigravity has no commands/ plugin component. Folded into SKILL.md's ## Common operations section instead.
  • rules/ — would risk colliding with user project rules. Users opt in by dropping their own under .agents/rules/.
  • permissions field in plugin.json — fabricated by the third-party antigravity-plugins community skill; no real Google-shipped plugin uses it. We pin to the verified-minimal shape and have a regression test.
  • Workspace-scoped install by default — the global install at ~/.gemini/config/plugins/mempalace/ is the canonical UX. Workspace-scoped install is documented in hooks/antigravity/README.md.
  • PreToolUse / PostToolUse hooks — out of scope for v1, surface is real and noted for future work.

Constraints honoured

  • bash 3.2.57 (macOS default): no mapfile/readarray/declare -A/${var^^}; sentinel-guarded JSON parser uses sed -n 'Np' like the existing Claude Code hook.
  • Verbatim guarantee: wake injection passes mempalace wake-up stdout through unchanged via json.dumps; never paraphrased.
  • Performance budgets: hook scripts return {} in <500ms on every kill-switch / gate path (test asserts <1.5s on CI), wake hook enforces a 500ms hard timeout on mempalace wake-up.
  • Zero new runtime dependencies: the hooks only use bash + python3 (already required by every existing hook).
  • Privacy: no telemetry, no external API, no phone-home.
  • Strictly additive: existing Claude/Codex hooks unchanged; existing tests continue to pass.

Critical safeguards

  • Stop hook NEVER emits {"decision":"continue"}. That output would force Antigravity into an infinite agent re-execution loop. The save hook's mempal_emit_stop_pass hard-codes printf '{}\n'. Tested directly in test_save_hook_never_emits_decision_continue.
  • MEMPAL_SAVE_INTERVAL floored to >= 1. A user setting MEMPAL_SAVE_INTERVAL=0 (thinking it disables saves) would otherwise bash-divide-by-zero. Tested in test_save_hook_floors_zero_save_interval_to_avoid_div_by_zero.
  • fullyIdle == false defers. Background commands still running means the transcript is in motion; better to skip than ingest a half-finished transcript.
  • Uninstall is basename-guarded AND plugin.json-name-guarded. Refuses to wipe a directory whose basename isn't mempalace, refuses if the directory has no plugin.json or names a different plugin. Tested in three separate refusal tests.
  • __PLUGIN_DIR__ substitution + relative-path absolutization. The cursor PR review caught that a relative --install-dir would bake a relative path into hooks.json that Antigravity couldn't resolve at runtime. Tested in test_relative_install_dir_is_absolutized.

Test plan

  • uv run pytest tests/ --ignore=tests/benchmarks -v2314 passed, 3 skipped, 1 unrelated warning
  • uv run ruff check . → all checks passed
  • uv run ruff format --check . → 139 files already formatted
  • bash -n clean on lib/common.sh, save hook, wake hook, install.sh
  • Local install at ~/.gemini/config/plugins/mempalace/ succeeds
  • All 8 expected files present after install with correct executable bits
  • Rendered hooks.json carries absolute paths (no __PLUGIN_DIR__ leak)
  • Both hooks fire correctly with realistic camelCase JSON payload
  • Wing inference picks wing_mempalace from workspacePaths[0]
  • State files all antigravity_*-namespaced (no Claude/Codex collision)
  • Second install.sh run produces byte-identical output (md5 snapshots match)
  • Uninstall removes only mempalace, leaves all 6 sibling Google plugins (firebase, chrome-devtools-plugin, google-antigravity-sdk, android-cli-plugin, custom-engineering-skills, modern-web-guidance-plugin) untouched
  • mempalace-mcp is on $PATH
  • Smoke-test the rendered plugin against a running Antigravity IDE — confirm MCP server mempalace shows in the MCP store, skill mempalace appears in the skill list, Stop event fires save hook on conversation end, PreInvocation event fires wake hook on first model call

New tests (56)

  • tests/test_antigravity_plugin_manifest.py11 tests — schema contract on .antigravity-plugin/, including guards against fabricated fields and symlink leaks.
  • tests/test_antigravity_hooks_shell.py31 tests — invokes bash hooks via subprocess, asserts {} on every failure path, kill-switch coverage (env vars + config.json + palace nuke), divide-by-zero floor, traversal rejection, namespacing, wing inference, decision-continue refusal.
  • tests/test_antigravity_hooks_install.py14 tests--dry-run side-effect-free, real install layout, executable bits preserved, byte-identical idempotent re-runs, basename-match uninstall safety + two further refusal modes, relative-path absolutization, unknown-arg failure. Skipped on Windows.

Branching

Branched from origin/main (per pre-flight constraint), targets develop for normal feature flow.

References

Made with Cursor

…ests)

Adds first-class integration with Google's Antigravity IDE
(https://antigravity.google/) as a third sibling to the existing
Claude Code and Codex hook integrations. Strictly additive — no
existing files in main are restructured.

What ships
----------

* `.antigravity-plugin/` — verified-minimal plugin package:
  * `plugin.json` with `{"name": "mempalace"}` (no fabricated fields)
  * `mcp_config.json` registering the `mempalace-mcp` stdio server
  * `hooks.json.tmpl` templated with `__PLUGIN_DIR__` substitution
  * `skills/mempalace/SKILL.md` (real file — no symlinks)
* `hooks/antigravity/`:
  * `lib/common.sh` — shared bash 3.2.57-compatible helpers with
    sentinel-guarded camelCase JSON parser, antigravity_*-namespaced
    state files, every existing kill switch, `MEMPAL_SAVE_INTERVAL >= 1`
    floor (no /0), and fail-open emitters
  * `mempal_save_hook_antigravity.sh` — Stop event handler:
    increments per-conversation counter, defers when fullyIdle=False
    or terminationReason=error, validates transcriptPath against
    `..` traversal, spawns `mempalace mine --mode convos` in a
    detached subprocess with a per-conversation pending marker,
    ALWAYS emits `{}` (never `{"decision":"continue"}` — that would
    force an infinite agent loop)
  * `mempal_wake_hook_antigravity.sh` — PreInvocation handler gated
    to invocationNum==1 with an atomic mkdir loop guard, runs
    `mempalace wake-up` with a 500ms hard timeout, emits verbatim
    output as `{"injectSteps":[{"ephemeralMessage":"..."}]}` or
    `{}` on any failure
  * `install.sh` — idempotent installer with cmp-gated copies,
    `__PLUGIN_DIR__` substitution, relative path absolutization,
    `--dry-run`, and basename-guarded `--uninstall` (refuses to
    wipe a directory whose basename isn't `mempalace`)
  * `INVESTIGATION.md` — verbatim quotes + URLs + dates from the
    five official Antigravity doc pages, recording every surface
    shipped and every surface deliberately omitted
    (PreCompact equivalent, slash-commands, rules/, plugin
    permissions field — the latter is third-party fabrication)
  * `STDIN_SHAPE.md` — exact stdin/stdout contract per event with
    worked examples
  * `README.md` — local hook docs + troubleshooting
* `examples/antigravity/{hooks.json,mcp_config.json,README.md}` —
  standalone configs for users who don't want the full installer
* `website/guide/antigravity.md` + sidebar entry — VitePress guide
* Updates to `README.md`, `CHANGELOG.md` (Unreleased), `hooks/README.md`

Tests (56 new, all passing)
---------------------------

* `tests/test_antigravity_plugin_manifest.py` (11 tests) — schema
  contract on the in-repo `.antigravity-plugin/` directory, including
  guards against re-introducing the fabricated `permissions` field
  and against any symlink leak.
* `tests/test_antigravity_hooks_shell.py` (31 tests) — invokes the
  bash hooks via subprocess with synthetic camelCase stdin, asserts
  `{}` on every failure path, kill-switch coverage (env vars +
  config.json + palace nuke), divide-by-zero floor, transcript
  traversal rejection, namespacing, wing inference, and the hard
  refusal to ever emit `decision=continue` from the Stop hook.
* `tests/test_antigravity_hooks_install.py` (14 tests) — `--dry-run`
  side-effect-free, real install layout, executable bits preserved,
  byte-identical idempotent re-runs (md5 + filecmp), basename-match
  uninstall safety, refusal when plugin.json is missing or names a
  different plugin, relative path absolutization. Skipped on Windows.

Verification
------------

* `uv run pytest tests/ --ignore=tests/benchmarks -v` → 2314 passed,
  3 skipped (Windows), 1 unrelated warning
* `uv run ruff check .` → all checks passed
* `uv run ruff format --check .` → 139 files already formatted
* `bash -n` clean on common.sh, both hook scripts, install.sh
* Local install at `~/.gemini/config/plugins/mempalace/` verified end-
  to-end: layout correct, paths absolutized in hooks.json, both hooks
  fire with realistic camelCase JSON in <1s, wing inference picks
  `wing_mempalace` from workspacePaths[0], state files all
  `antigravity_*`-namespaced, second `install.sh` run produces
  byte-identical output (md5 snapshots match), uninstall removes
  only the mempalace plugin and leaves all 6 sibling Google plugins
  untouched.

Constraints honoured
--------------------

bash 3.2.57 (no mapfile / readarray / declare -A / `${var^^}`),
verbatim guarantee on all wake injections, hooks <500ms / startup
injection <100ms target (kill-switch path returns in <1.5s in CI),
zero new runtime dependencies, no telemetry, no external API,
strictly additive (existing Claude/Codex hooks unchanged).

Refs: hooks/antigravity/INVESTIGATION.md for the full audit.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces first-class support for Google's Antigravity IDE, adding a new .antigravity-plugin/ package, an idempotent installer script (hooks/antigravity/install.sh), lifecycle hooks for background mining and startup memory injection, comprehensive tests, and documentation. The reviewer identified several key issues in the shell scripts and Python helpers: a critical bug in the save hook where a background subshell attempts to wait on a sibling process (which fails immediately and prematurely deletes the pending marker), an issue where the Python JSON parser catches exceptions internally but still prints the success sentinel (defeating bash-side error detection), a potential octal parsing crash in bash arithmetic if MEMPAL_SAVE_INTERVAL contains leading zeros, path resolution issues when calling the mempalace CLI directly instead of using the resolved Python interpreter, and a redundant, no-op subshell directory change in the installer script.

Comment on lines +194 to +213
if command -v mempalace >/dev/null 2>&1; then
nohup mempalace mine "$TRANSCRIPT_DIR" \
--mode convos \
--wing "$WING" \
>> "$MEMPAL_AGY_LOG" 2>&1 < /dev/null &

MINE_PID=$!
mempal_log "stop" "$CONVERSATION_ID" "mine spawned pid=$MINE_PID wing=$WING"

# Schedule a marker-cleanup detach so the marker doesn't outlive a
# crashed mine. We can't `wait` because that would block the hook;
# instead, fire-and-forget a tiny watcher.
(
wait "$MINE_PID" 2>/dev/null
rm -f "$PENDING_FILE" 2>/dev/null
) >/dev/null 2>&1 < /dev/null &
else
mempal_log "stop" "$CONVERSATION_ID" "ERROR: mempalace CLI not on PATH; install or set MEMPAL_PYTHON"
rm -f "$PENDING_FILE" 2>/dev/null
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block contains two issues:

  1. Sibling Process Wait Bug (Critical): In bash, a background subshell ( wait "$MINE_PID" ... ) & cannot wait for a sibling process spawned by the parent shell; wait will fail immediately with an error and proceed to delete the PENDING_FILE instantly. This defeats the concurrency guard. Using a while kill -0 polling loop resolves this.
  2. Python Interpreter Path (Medium): Running mempalace directly relies on the global PATH. Running it via "$MEMPAL_PYTHON_BIN" -m mempalace ensures it uses the correctly resolved Python interpreter (respecting MEMPAL_PYTHON and virtual environments).
if "$MEMPAL_PYTHON_BIN" -m mempalace --version >/dev/null 2>&1; then
    nohup "$MEMPAL_PYTHON_BIN" -m mempalace mine "$TRANSCRIPT_DIR" \
        --mode convos \
        --wing "$WING" \
        >> "$MEMPAL_AGY_LOG" 2>&1 < /dev/null &

    MINE_PID=$!
    mempal_log "stop" "$CONVERSATION_ID" "mine spawned pid=$MINE_PID wing=$WING"

    # Schedule a marker-cleanup detach so the marker doesn't outlive a
    # crashed mine. We can't wait because that would block the hook;
    # instead, fire-and-forget a tiny watcher.
    (
        while kill -0 "$MINE_PID" 2>/dev/null; do
            sleep 1
        done
        rm -f "$PENDING_FILE" 2>/dev/null
    ) >/dev/null 2>&1 < /dev/null &
else
    mempal_log "stop" "$CONVERSATION_ID" "ERROR: mempalace is not runnable via $MEMPAL_PYTHON_BIN; install or set MEMPAL_PYTHON"
    rm -f "$PENDING_FILE" 2>/dev/null
fi

Comment thread hooks/antigravity/install.sh Outdated
Comment on lines +126 to +127
(cd "$OLDPWD" 2>/dev/null || cd .) >/dev/null 2>&1
local base="${PWD}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The subshell (cd "$OLDPWD" 2>/dev/null || cd .) does not affect the parent shell's working directory, making it a no-op. Furthermore, since the script does not change directories in the main shell, "$PWD" already correctly points to the user's invocation directory. This line can be safely removed.

Suggested change
(cd "$OLDPWD" 2>/dev/null || cd .) >/dev/null 2>&1
local base="${PWD}"
local base="${PWD}"

Comment thread hooks/antigravity/lib/common.sh Outdated
Comment on lines +169 to +172
try:
data = json.load(sys.stdin)
except Exception:
data = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The try/except block around json.load(sys.stdin) catches parse failures and sets data = {}, but the script still prints __MEMPAL_PARSE_OK__ on line 208. This prevents the calling bash scripts from detecting JSON parse failures via the sentinel check [ "$_marker" != "__MEMPAL_PARSE_OK__" ]. Letting the exception propagate naturally will cause the Python script to exit with a non-zero status and omit the sentinel, allowing the bash scripts to correctly trigger their defense-in-depth error logging.

Suggested change
try:
data = json.load(sys.stdin)
except Exception:
data = {}
data = json.load(sys.stdin)

Comment on lines +283 to +293
mempal_save_interval() {
local raw="${MEMPAL_SAVE_INTERVAL:-15}"
case "$raw" in
''|*[!0-9]*) printf '15'; return 0 ;;
esac
if [ "$raw" -lt 1 ] 2>/dev/null; then
printf '15'
return 0
fi
printf '%s' "$raw"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In bash, arithmetic expansions like $((COUNT % INTERVAL)) treat numbers with leading zeros as octal. If MEMPAL_SAVE_INTERVAL is configured with a leading zero (e.g., 08 or 09), the hook will crash with a value too great for base syntax error. Stripping leading zeros from the returned interval prevents this issue.

Suggested change
mempal_save_interval() {
local raw="${MEMPAL_SAVE_INTERVAL:-15}"
case "$raw" in
''|*[!0-9]*) printf '15'; return 0 ;;
esac
if [ "$raw" -lt 1 ] 2>/dev/null; then
printf '15'
return 0
fi
printf '%s' "$raw"
}
mempal_save_interval() {
local raw="${MEMPAL_SAVE_INTERVAL:-15}"
case "$raw" in
''|*[!0-9]*) printf '15'; return 0 ;;
esac
# Strip leading zeros to prevent bash treating it as octal in $(( ... ))
while [ "${raw}" != "${raw#0}" ] && [ "${#raw}" -gt 1 ]; do
raw="${raw#0}"
done
if [ "$raw" -lt 1 ] 2>/dev/null; then
printf '15'
return 0
fi
printf '%s' "$raw"
}


try:
completed = subprocess.run(
['mempalace', 'wake-up', '--wing', wing],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Running mempalace directly relies on the global PATH, which might not contain the executable if it was installed in a virtual environment. Using sys.executable with the -m flag ensures that the hook runs mempalace using the exact same Python interpreter that was resolved (respecting MEMPAL_PYTHON).

Suggested change
['mempalace', 'wake-up', '--wing', wing],
[sys.executable, '-m', 'mempalace', 'wake-up', '--wing', wing],



Five fixes for issues called out by the gemini-code-assist[bot] review.
Each gets a regression test that locks in the correction.

1. CRITICAL: marker-cleanup watcher used POSIX `wait` on a sibling pid
   (save hook). bash `wait` only works on direct children of the
   calling shell — the `( wait $MINE_PID ... ) &` subshell runs as a
   sibling of MINE_PID, so wait fails immediately and the pending
   marker is deleted within milliseconds, defeating the concurrency
   guard. Replace with `while kill -0 $MINE_PID; do sleep 1; done`,
   which queries pid existence regardless of parent-child relationship.
   Test: test_save_hook_marker_watcher_uses_kill_polling.

2. Bare `mempalace` console-script invocation in the save hook fails
   when the venv's bin/ is not on the hook's PATH (e.g. uv tool
   install in some configurations, manually managed virtualenvs).
   Switch to `"$MEMPAL_PYTHON_BIN" -m mempalace mine ...` so the
   resolved interpreter runs the package directly via
   mempalace/__main__.py. Tests:
   test_save_hook_uses_python_module_invocation,
   test_save_hook_missing_mempalace_python_module_does_not_crash.

3. Same issue in the wake hook's inner Python helper. Switch
   `['mempalace', 'wake-up', ...]` to `[sys.executable, '-m',
   'mempalace', 'wake-up', ...]` — sys.executable is the same
   interpreter that resolved MEMPAL_PYTHON in lib/common.sh.
   Test: test_wake_hook_uses_sys_executable_module_invocation.

4. The Python parser in lib/common.sh wrapped `json.load` in
   `try/except` and silently fell back to `data = {}`. The script
   then printed the `__MEMPAL_PARSE_OK__` sentinel even on parse
   failure, so the bash sentinel-check on the caller side
   (`[ "$_marker" != "__MEMPAL_PARSE_OK__" ]`) never triggered the
   defense-in-depth `input parse failed` branch. Remove the
   try/except so the exception propagates, Python exits non-zero,
   and the sentinel is omitted on bad JSON. The traceback still
   lands in antigravity_last_python_err.log for debugging.
   Test: test_common_sh_parser_omits_sentinel_on_malformed_json.

5. `mempal_save_interval()` failed to strip leading zeros from
   MEMPAL_SAVE_INTERVAL. Values like "08" or "09" then crashed the
   modulo step `$((COUNT % INTERVAL))` because bash arithmetic
   parses tokens starting with `0` as octal, and 8/9 are not valid
   octal digits ("value too great for base"). Strip leading zeros
   while preserving the literal "0" (which is then floored to 15).
   Test: test_save_hook_handles_leading_zero_save_interval (4 cases).

Plus one cosmetic fix in install.sh: removed a no-op `(cd "$OLDPWD"
2>/dev/null || cd .) >/dev/null 2>&1` line in mempal_absolutize().
The subshell cd doesn't affect the parent shell, and the installer
never cd's in the main shell anyway, so $PWD is already correct.

Verification:
* 9 new regression tests, all 65 antigravity tests pass
* full repo: 2323 passed (was 2314), 3 skipped, 1 unrelated warning
* ruff check + ruff format --check both clean across 139 files
* bash -n clean on all four shell files
* clean reinstall to ~/.gemini/config/plugins/mempalace/ succeeds
* idempotent re-run produces zero file writes (cmp-gated)
* both hooks return {} exit 0 with synthetic camelCase stdin

Co-authored-by: Cursor <cursoragent@cursor.com>
@igorls

igorls commented May 29, 2026

Copy link
Copy Markdown
Member

Review

Unusually careful, genuinely defensive shell work — fail-open on every path, layered kill switches, sentinel-guarded Python JSON parsing with input sanitization, bash 3.2.57 compat throughout, ..-traversal rejection, dry-run, idempotent installer with cmp-gated byte-identical re-runs, and a basename-and-plugin.json-name-guarded uninstall that refuses to wipe anything that isn't a mempalace plugin. Privacy is honored (0600 on logs that echo paths), wake injection is verbatim via json.dumps, and plugin.json is minimal ({"name":"mempalace"}) — no version to drift. The notes below are mostly cross-cutting; few are bugs.

Cleaner philosophy than the sibling Cursor PR (#1632): the Stop hook only background-mines and never emits a token-spending followup — which is the right call under CLAUDE.md's "Background everything — zero tokens in the chat window."

🟡 Specifics

  • Counter write is not atomic, despite the comment saying it is. printf '%s' "$COUNT" > "$COUNTER_FILE" truncates-then-writes. The Cursor PR's mempal_write_counter_atomic (temp + mv) is the correct pattern — match it here, or fix the "written atomically" comment. Low impact (concurrent Stop fires are unlikely) but the comment is wrong.
  • Synchronous python -m mempalace --version probe on the save path, in the foreground before the mine is backgrounded. If the package import is heavy (ChromaDB et al.), that foreground probe alone can blow the <500ms save budget the brief sets. Move it inside the backgrounded subshell, or cache the result.
  • mempal_save_interval octal-strip + zero-floor is correct and well-reasoned. The kill -0 polling subshell to clean the pending marker (given the sibling-PID constraint that rules out wait) is clever and well-documented.

Cross-cutting (also raised on the Cursor PR #1632)

  • Unbounded state-file growth. Per-conversation counter + .pending files and a woke_<conv> directory in ~/.mempalace/hook_state/ are never GC'd — one+ artifact per conversation, forever. A TTL sweep would help. Low severity.
  • Python cold-start vs. perf budget. Every fire spawns Python ≥1× (parse), often 2× (kill-switch config.json read). PreInvocation fires before every model call before the invocationNum==1 gate; ~30–80ms ×2 of interpreter startup makes the brief's 100ms startup ceiling optimistic. Worth measuring.
  • Merge collision with feat: add Cursor IDE support (hooks, plugin, skill, docs, tests) #1632. Both edit website/.vitepress/config.mts, README.md, CHANGELOG.md, hooks/README.md. Sequence the merges.

Minor

  • mempal_infer_wing yields wing_<slug> (default wing_sessions), while the Cursor PR uses a bare basename (default cursor_session) — so the same workspace mined from both IDEs lands in different wings. Intentional per-IDE, but a cross-IDE consistency wrinkle worth a deliberate decision.

Mergeable in principle — no blockers. The atomic-counter and foreground---version-probe items are the two I'd want addressed; the rest is shared hygiene.

…tate-file GC

Addresses igorls' review on PR MemPalace#1633 (antigravity branch only):

- Atomic counter write: add mempal_write_counter_atomic (same-dir temp +
  mv -f rename) and use it in the save hook, replacing the truncate-then
  -write printf that the comment falsely called "atomic". Concurrent
  readers now always see a complete value.
- Background the expensive probe: `mempalace --version` pays the full
  chromadb/onnx cold-start import (the mine subparser imports
  mempalace.miner before argparse handles --version), so running it in
  the foreground blew the <500ms save budget. Probe + mine + pending
  -marker cleanup now run in one detached subshell; the foreground
  returns immediately. This also retires the kill -0 watcher (the prior
  gemini fix) since cleanup is now sequential within the mine's own
  shell, removing the sibling-PID hazard entirely.
- State-file GC: add mempal_state_ttl_days (default 30, env
  MEMPAL_STATE_TTL_DAYS) and mempal_gc_stale_state, a daily-throttled
  sweep (antigravity_last_sweep marker) that removes stale
  antigravity_save_count_*, antigravity_pending_*, and
  antigravity_woke_* artifacts. Called after the kill-switch check so a
  disabled hook touches nothing; specific name globs leave shared logs
  untouched.

Tests: atomic-counter behavior + no-temp-leftover, single-subshell
structure (no wait/kill -0/MINE_PID), backgrounded-probe timing (3s
stub returns in <2s), async log polling for the missing-module path,
GC sweep/throttle/TTL-validation, and GC gated by the kill switch.
Also hardens the wake-missing test to pin MEMPAL_PYTHON so a shell
-exported interpreter can't defeat the simulation.

bash 3.2.57 safe, fail-open on every path, {}-only Stop output.

Co-authored-by: Cursor <cursoragent@cursor.com>
@undeadindustries

Copy link
Copy Markdown
Contributor Author

Thanks @igorls — pushed df295bd addressing the review. Scope here is the Antigravity branch only; cross-IDE/Cursor items are flagged for the Cursor PR (#1632) / maintainer coordination as noted below.

Must-fix

1. Non-atomic counter write — Fixed. Added mempal_write_counter_atomic() to lib/common.sh (same-directory temp file + mv -f, an atomic rename on one filesystem, with a direct-write fallback if mktemp fails). The save hook now calls it instead of the truncate-then-write printf > "$COUNTER_FILE", and the misleading "written atomically" comment now describes the real mechanism. The temp stays antigravity_save_count_<conv>.XXXXXX-namespaced. Tests: counter still increments correctly across fires and leaves no temp behind.

2. Expensive foreground --version probe — Fixed. Confirmed via mempalace/cli.py that the mine subparser imports mempalace.miner (→ palace → backends → chromadb/onnx) during parser construction, so mempalace --version pays the full cold-start import. The probe, the mine, and the pending-marker cleanup now run in one detached subshell; the foreground drops the marker, logs, spawns, and returns immediately. New timing test: with a stub interpreter that sleeps 3s on -m mempalace, the hook returns in <2s — proving the probe is off the foreground.

This also retires the kill -0 watcher from the prior gemini fix: marker cleanup is now sequential inside the mine's own subshell, so the sibling-PID wait hazard is structurally gone (no wait, no kill -0, no MINE_PID capture).

Non-blocking hygiene (done in this PR)

State-file GC — Added mempal_state_ttl_days() (default 30, env MEMPAL_STATE_TTL_DAYS, integer-floored/leading-zero-stripped) and mempal_gc_stale_state(). The sweep is throttled to at most once per 24h via an antigravity_last_sweep marker (mtime check), so it adds a single stat to the common path. It removes stale antigravity_save_count_*, antigravity_pending_*, and antigravity_woke_* artifacts older than the TTL; the name globs are specific, so antigravity_hook.log, the input/error logs, and the sweep marker itself are never touched. BSD-find (macOS) and GNU-find compatible. Called right after the kill-switch check so a disabled hook touches nothing. Tests cover sweep, daily throttle, stale-marker re-run, TTL validation, and kill-switch gating.

Measure-first / deferred

  • Python cold-start vs perf budget (PreInvocation) — measure-then-decide. Phase work above already removes the heaviest foreground cost on the save path. The PreInvocation stdin parse spawns python once before the invocationNum==1 gate; I'd rather measure that on real hardware than reorder speculatively in this PR.
  • mempal_infer_wing cross-IDE naming (wing_<slug> / default wing_sessions here vs the bare basename / cursor_session in feat: add Cursor IDE support (hooks, plugin, skill, docs, tests) #1632) — this is a cross-IDE alignment decision that shouldn't be unilaterally changed on the Antigravity branch. Deferring to coordinate with the Cursor PR so both land on one convention.
  • Merge-sequencing of shared files (website/.vitepress/config.mts, README.md, CHANGELOG.md, hooks/README.md) — acknowledged; these overlap with feat: add Cursor IDE support (hooks, plugin, skill, docs, tests) #1632 and should be sequenced at merge time rather than pre-empted here.

Full antigravity suite (74 tests) green; full repo suite (2331 passed, 3 skipped) green; ruff check/format clean; bash -n clean on all four shell files; local reinstall is byte-identical idempotent and both hooks return {} exit 0 on synthetic camelCase stdin.

@igorls

igorls commented May 30, 2026

Copy link
Copy Markdown
Member

Maintainer review (on df295bd) — thanks for the thorough turnaround on the last round: atomic counter via mempal_write_counter_atomic, the backgrounded --version probe, and the throttled GC sweep all look good, and retiring the kill -0 watcher is a nice structural simplification. Two things before merge:

  1. The new GC throttle uses GNU-only date -r FILE, which breaks on macOS — the platform you explicitly target (bash 3.2.57). You were careful to make the GC sweep BSD/GNU-find compatible, but the once-per-24h throttle reads its marker mtime with date -r "$marker" '+%s' (the mempal_gc_stale_state check), and the pending-marker staleness guard does the same with date -r "$PENDING_FILE" '+%s'. On BSD/macOS date -r treats its argument as epoch seconds, not a file path, so both probes fail open: the throttle never returns early (it runs the 3-pass sweep on every Stop instead of once/day), and the pending guard falls through to rm -f — defeating the concurrent-save lock you just hardened in this same commit. Linux CI stays green, so the new throttle/marker tests don't catch it. A small portable helper — try stat -f %m, fall back to stat -c %Y, then date -r last — fixes both call sites and makes the "macOS compatible" claim hold.

  2. No normalizer for the Antigravity transcript schema (medium, still open). The Stop hook runs mempalace mine … --mode convos, but normalize.py has no Antigravity detector, so an unrecognized .jsonl falls to paragraph chunking and stores raw JSON envelopes as drawers. Verbatim is preserved, but the drawers are noisy vs the Claude/Codex/Gemini paths. Either add a _try_antigravity_jsonl() detector (mirroring _try_gemini_jsonl) + a test, or scope-document the limitation prominently.

The deferred cross-IDE wing-naming and the shared-file merge-sequencing with #1632 are fine to settle at coordination/merge time — agreed those shouldn't be pre-empted here.

undeadindustries and others added 3 commits May 31, 2026 10:41
`uv tool install mempalace` / `pipx install` place the mempalace
console scripts in an isolated environment whose interpreter is not
the system python3. mempal_resolve_python previously resolved
`command -v python3`, landing on a Python that cannot import
mempalace: the `-m mempalace --version` probe failed and mining
silently never fired (hit by a real user on PR MemPalace#1633).

Resolution now derives the interpreter from the mempalace-mcp /
mempalace console-script shebang on PATH (the same script the MCP
server launches) before falling back to python3. It is pure shebang
parsing + stat — no Python subprocess at source time — so the hook
performance budget is preserved. An env-style `#!/usr/bin/env python`
shebang and a non-executable interpreter are both rejected and fall
through. MEMPAL_PYTHON remains the explicit override.

Adds 6 resolver regression tests, documents resolution + MEMPAL_PYTHON
in the guide and hooks README (fixing the stale `command -v mempalace`
note), and a CHANGELOG entry.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants