test(workstream): harden Antigravity managed support

This commit is contained in:
AkitaOnRails
2026-08-01 10:28:02 -03:00
parent 30b9040b42
commit fc0b30657a
9 changed files with 370 additions and 97 deletions
+9 -7
View File
@@ -105,23 +105,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
pages reinforced by several operators without changing existing scores at
the default or for pages with zero or one identified reader (#336).
- `run` and `show` accept `antigravity` (aliases `antigravity-cli`, `agy`) as a
- `run` accepts `antigravity` (aliases `antigravity-cli`, `agy`) as a
managed harness, so an Antigravity session joins the same workstream as the
Claude Code or Codex sessions on the same checkout. `agy` accepts no
caller-chosen id for a new conversation, so a fresh launch injects nothing and
the id is linked by the hooks or discovered afterwards; a linked resume passes
`--conversation <id>`, and `--continue` / `-c` is respected as an explicit user
choice. `--yolo` maps to `--dangerously-skip-permissions`, and the utility
caller-chosen id for a new conversation, so a fresh launch injects no
selector and the id is linked by the hooks or discovered afterwards; a
linked resume passes `--conversation <id>`, and `--continue` / `-c` is
respected as an explicit user choice. `--yolo` maps to
`--dangerously-skip-permissions`, and the utility
subcommands (`models`, `plugin`, `update`, …) pass through without a selector.
Conversation discovery reads the per-conversation SQLite databases under
`~/.gemini/antigravity-cli/conversations/`: the id is the file name and the
workspace comes from two self-describing protobuf fields, so only conversations
workspace comes from two observed protobuf fields, so only conversations
opened on the current directory are offered and a database from another `agy`
version is skipped instead of failing the listing. Step payloads are
undocumented, unversioned protobuf, so conversation text is deliberately not
decoded — the visible-event ledger for this harness comes from lifecycle-hook
capture, and transcript export fails with a message saying so. Antigravity is
not part of the no-argument auto-detection pool. (#NNN)
not part of the no-argument auto-detection pool. The native contract was
verified against Antigravity CLI v1.1.7 (#345).
### Fixed
- `run` on Windows now resolves npm-style harness installs through `PATHEXT`
+3 -3
View File
@@ -193,8 +193,8 @@ priors are at the [bottom](#influences-and-prior-art).
the workstream immediately. If a linked native transcript was deleted,
ai-memory detects the orphan before launch and starts fresh; `--fresh` forces
that recovery for one harness. Managed mode currently covers Claude Code,
Codex, OpenCode, Pi, Crush, Kimi Code, OMP, and Grok Build CLI; direct harness
launches remain unchanged. See
Codex, OpenCode, Pi, Crush, Kimi Code, OMP, Grok Build CLI, and Antigravity
CLI; direct harness launches remain unchanged. See
[Managed cross-harness workstreams](docs/managed-workstreams.md).
- **"Quit at 4 PM, pick up at 9 AM in a different agent."** The
classic. SessionStart hook in the next supported hook client prepends a
@@ -882,7 +882,7 @@ diagram, crate breakdown, schema notes, and invariants.
|---|---|
| [`docs/install.md`](docs/install.md) | **Installation cookbook.** Every agent CLI, every alternative (curl, source build, no-docker, no-auth), and the server-on-a-different-machine (homelab/LAN) walkthrough. Read after the Quick start if your setup doesn't match the happy path. |
| [`docs/usage.md`](docs/usage.md) | Handoffs, proactive memory queries, slim routing snippet + managed Agent Skills, migration from other memory tools, web UI, raw-wiki inspection, and rules-vs-facts workflow. |
| [`docs/managed-workstreams.md`](docs/managed-workstreams.md) | Optional `ai-memory run` continuity across Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, and Grok Build CLI: automatic harness selection, native resume, argument forwarding, ledger search, privacy, and recovery. |
| [`docs/managed-workstreams.md`](docs/managed-workstreams.md) | Optional `ai-memory run` continuity across Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, Grok Build CLI, and Antigravity CLI: automatic harness selection, native resume, argument forwarding, ledger search, privacy, and recovery. |
| [`docs/managed-harness-contributions.md`](docs/managed-harness-contributions.md) | Protocol and acceptance bar for contributors adding managed resume, read-only transcript import, and startup context delivery to another harness. |
| [`docs/marker-file.md`](docs/marker-file.md) | `.ai-memory.toml` workspace/project routing for multi-client trees, mono-repos, worktrees, and work/personal separation. |
| [`docs/auto-scope.md`](docs/auto-scope.md) | `[auto_scope]` modes for shared servers: default single-slot routing, session-aware isolation, and multi-user `per_actor` behavior. |
+59 -15
View File
@@ -14,6 +14,7 @@ use rusqlite::{Connection, OpenFlags, params};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest as _, Sha256};
use uuid::Uuid;
use crate::ManagedHarness;
@@ -88,10 +89,10 @@ pub async fn export_transcript(
// blob whose step-type enum is unversioned, so message text cannot be
// decoded without guessing at a schema that changes between `agy`
// releases. Conversation identity and workspace are read (they are
// stable, self-describing fields); the visible-event ledger for this
// harness comes from lifecycle-hook capture instead.
// stable fields observed in current metadata); the visible-event
// ledger for this harness comes from lifecycle-hook capture instead.
return Err(anyhow!(
"antigravity conversations expose no decodable transcript; this session's events come from hook capture"
"antigravity conversations expose no decodable transcript; this session's events come from hook capture"
));
}
let path = locate_session_file(harness, home, cwd, session_dir, native_session_id)?
@@ -1575,6 +1576,9 @@ fn locate_session_file(
let root = session_root(harness, home, session_dir);
if harness == ManagedHarness::Antigravity {
// The conversation id is the file name, so no scan is ever needed.
if Uuid::parse_str(id).is_err() {
return Ok(None);
}
let exact = root.join(format!("{id}.db"));
return Ok(exact.is_file().then_some(exact));
}
@@ -1760,7 +1764,7 @@ fn antigravity_session_header(path: &Path) -> Result<Option<(String, PathBuf)>>
let Some(id) = path.file_stem().and_then(|stem| stem.to_str()) else {
return Ok(None);
};
if !valid_native_session_id(id) {
if !valid_native_session_id(id) || Uuid::parse_str(id).is_err() {
return Ok(None);
}
let Ok(connection) = Connection::open_with_flags(
@@ -1770,7 +1774,7 @@ fn antigravity_session_header(path: &Path) -> Result<Option<(String, PathBuf)>>
return Ok(None);
};
let blob = connection.query_row(
"SELECT data FROM trajectory_metadata_blob LIMIT 1",
"SELECT data FROM trajectory_metadata_blob WHERE id = 'main' LIMIT 1",
[],
|row| row.get::<_, Vec<u8>>(0),
);
@@ -1819,6 +1823,9 @@ fn protobuf_field(message: &[u8], field: u64) -> Option<&[u8]> {
fn protobuf_varint(bytes: &[u8]) -> Option<(u64, usize)> {
let mut value = 0u64;
for (index, byte) in bytes.iter().take(10).enumerate() {
if index == 9 && *byte > 1 {
return None;
}
value |= u64::from(byte & 0x7f) << (index * 7);
if byte & 0x80 == 0 {
return Some((value, index + 1));
@@ -3234,15 +3241,38 @@ mod tests {
);
}
/// Build the two protobuf layers `agy` writes: an outer message whose
/// field 1 holds a nested message whose field 1 is the workspace URI.
fn push_protobuf_varint(output: &mut Vec<u8>, mut value: u64) {
loop {
let byte = (value & 0x7f) as u8;
value >>= 7;
output.push(if value == 0 { byte } else { byte | 0x80 });
if value == 0 {
break;
}
}
}
fn push_protobuf_bytes(output: &mut Vec<u8>, field: u64, value: &[u8]) {
push_protobuf_varint(output, (field << 3) | 2);
push_protobuf_varint(output, u64::try_from(value.len()).unwrap());
output.extend_from_slice(value);
}
/// Build the two protobuf layers observed in `agy` v1.1.7 metadata. The
/// unrelated fields ensure discovery searches by field number instead of
/// assuming the workspace URI is the entire message.
fn antigravity_metadata(uri: &str) -> Vec<u8> {
let mut nested = vec![0x0a];
nested.push(u8::try_from(uri.len()).unwrap());
nested.extend_from_slice(uri.as_bytes());
let mut outer = vec![0x0a];
outer.push(u8::try_from(nested.len()).unwrap());
outer.extend_from_slice(&nested);
let mut nested = Vec::new();
push_protobuf_varint(&mut nested, 2 << 3);
push_protobuf_varint(&mut nested, 7);
push_protobuf_bytes(&mut nested, 1, uri.as_bytes());
push_protobuf_bytes(&mut nested, 4, b"fixture-metadata");
let mut outer = Vec::new();
push_protobuf_bytes(&mut outer, 3, b"unrelated");
push_protobuf_bytes(&mut outer, 1, &nested);
push_protobuf_varint(&mut outer, 5 << 3);
push_protobuf_varint(&mut outer, 1);
outer
}
@@ -3276,13 +3306,15 @@ mod tests {
}
/// The conversation id is the file name and the workspace comes from the
/// metadata blob, so a checkout only sees its own conversations.
/// metadata blob, so a checkout only sees its own conversations. The long
/// component forces both protobuf layers to use multi-byte varint lengths.
#[tokio::test]
async fn antigravity_lists_only_conversations_from_this_workspace() {
let temp = tempfile::TempDir::new().unwrap();
let root = temp.path().join(".gemini/antigravity-cli/conversations");
fs::create_dir_all(&root).unwrap();
let cwd = temp.path().join("checkout");
fs::write(root.parent().unwrap().join("outside.db"), b"outside").unwrap();
let cwd = temp.path().join(format!("checkout-{}", "x".repeat(140)));
fs::create_dir_all(&cwd).unwrap();
let mine = "a0d5ac62-2501-4780-b783-76d159c56cb3";
let theirs = "9576275f-7c4e-4709-b372-22d1ad2a0af8";
@@ -3315,6 +3347,16 @@ mod tests {
)
.unwrap()
);
assert!(
!native_session_exists(
ManagedHarness::Antigravity,
temp.path(),
&cwd,
None,
"../outside"
)
.unwrap()
);
}
/// A database whose metadata is shaped differently — an older or newer
@@ -3397,5 +3439,7 @@ mod tests {
assert_eq!(protobuf_field(&message, 9), None);
// Truncated input ends the walk instead of panicking on a slice.
assert_eq!(protobuf_field(&[0x22, 0x10, b'x'], 4), None);
// A ten-byte varint may carry only one payload bit in its final byte.
assert_eq!(protobuf_varint(&[0xff; 10]), None);
}
}
+3 -2
View File
@@ -300,8 +300,9 @@ Top-line rules carved into the codebase:
## 15. Managed workstreams use a portable ledger, not native format conversion
Managed cross-harness continuity is explicitly opt-in through `ai-memory run`.
Direct Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, and Grok Build
CLI launches retain the existing hook and single-use handoff behavior. There is
Direct Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, Grok Build CLI,
and Antigravity CLI launches retain the existing hook and single-use handoff
behavior. There is
no process-global mode or manual harness switch: the wrapper selects the
current repository/worktree workstream and each adapter applies that harness's
native create/resume syntax.
+1 -1
View File
@@ -1257,7 +1257,7 @@ docker run --rm akitaonrails/ai-memory:latest --help # full subcommand tree
| Subcommand | Pattern | What it does |
|---|---|---|
| `serve` | `docker compose up -d` (already done) | Run the HTTP MCP server |
| `run [harness] [args...]` | host wrapper or native binary | Opt into one managed cross-harness workstream; omit the harness to resume the newest usable local session, or name Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, or Grok Build CLI explicitly; exact `--yolo` and `--fresh` flags are wrapper-owned and other native arguments pass through |
| `run [harness] [args...]` | host wrapper or native binary | Opt into one managed cross-harness workstream; omit the harness to resume the newest usable local session, or name Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, Grok Build CLI, or Antigravity CLI explicitly; exact `--yolo` and `--fresh` flags are wrapper-owned and other native arguments pass through |
| `workstream-search [query]` | managed child or thin HTTP client | Search the complete visible managed-workstream ledger; the managed child receives its workstream id automatically |
| `status` | `docker exec` | Counts, paths, derived-index diagnostics, and passive LLM/embedding provider health |
| `search "<query>"` | `docker exec` | Wiki FTS5 search + bounded source authority; use MCP `memory_query` for entity/graph/vector RRF |
+24 -18
View File
@@ -1,10 +1,10 @@
# Adding a managed harness
Managed-workstream support is narrower than MCP or lifecycle-hook support. This
release can manage Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP, and
Grok Build CLI. Gemini CLI, Devin CLI, Cursor, and the other integrations in
the README support matrix do not become managed merely because ai-memory can
capture their hooks.
release can manage Claude Code, Codex, OpenCode, Pi, Crush, Kimi Code, OMP,
Grok Build CLI, and Antigravity CLI. Gemini CLI, Devin CLI, Cursor, and the
other integrations in the README support matrix do not become managed merely
because ai-memory can capture their hooks.
A managed adapter must preserve a harness's real native session, deliver the
portable workstream delta exactly once, and import only visible history without
@@ -78,8 +78,9 @@ continue, or fork selector.
## 4. Discover and export read-only
Implement candidate discovery and incremental export in
`crates/ai-memory-workstream/src/transcript.rs`.
Implement candidate discovery in `crates/ai-memory-workstream/src/transcript.rs`.
Implement incremental export only when a documented or repeatably observed
native format exposes visible conversation records without private state.
The adapter must:
@@ -87,17 +88,19 @@ The adapter must:
- honor documented store-root environment and command-line overrides;
- open SQLite stores read-only and never create, migrate, vacuum, or repair
them;
- tolerate an incomplete final JSONL record or an in-progress tool call without
advancing past it;
- emit deterministic source record and event ids;
- resume from a persisted source cursor without duplicates;
- normalize visible user/assistant messages, completed tool calls/results, and
compaction summaries; and
- when export is supported, tolerate an incomplete final record or in-progress
tool call without advancing past it;
- when export is supported, emit deterministic source record and event ids and
resume from a persisted source cursor without duplicates;
- normalize only visible user/assistant messages, completed tool calls/results,
and compaction summaries; and
- exclude system/developer prompts, hidden reasoning, binary payloads,
credentials, provider metadata, and unsupported records.
Extraction gaps should become bounded loss annotations. They must not cause the
adapter to copy a private record "just in case."
Extraction gaps should become bounded loss annotations. If the conversation
payload is opaque or undocumented, return such an annotation and rely on
sanitized lifecycle-hook capture instead of guessing. Never copy a private
record "just in case."
## 5. Deliver context before acknowledging it
@@ -147,10 +150,13 @@ A managed-harness PR should include focused coverage for:
- deterministic fake-process acceptance in
`scripts/managed-workstream-acceptance.sh`; and
- a manual real-harness pass that switches into the new harness, records
successful delivery of its assigned context delta, persists a new assistant
event, and resumes its original native session when revisited. Do not make
pass/fail depend on the model quoting packet text: some harnesses externalize
large hook results to a file, making recall depend on a model tool-use choice.
successful delivery of its assigned context delta, and resumes its original
native session when revisited. Require a new assistant event when the adapter
has a readable transcript. A deliberately hook-only adapter must instead
prove that a correlated lifecycle observation was persisted and document the
bounded transcript-loss annotation. Do not make pass/fail depend on the model
quoting packet text: some harnesses externalize large hook results to a file,
making recall depend on a model tool-use choice.
The deterministic phase remains credential-free and suitable for frequent
local runs. Real model calls stay opt-in and outside CI. Record the tested CLI
+37 -26
View File
@@ -158,10 +158,12 @@ is labelled completed evidence and must never be replayed as a pending call.
| Kimi Code | native default creation | `--session <id>` | `$KIMI_CODE_HOME/sessions/*/*/agents/main/wire.jsonl` |
| OMP | native default creation | `--resume=<id>` | `~/.omp/agent/sessions/**/*.jsonl` |
| Grok Build CLI | generated `--session-id` | `--resume <id>` | `$GROK_HOME/sessions/*/*/chat_history.jsonl` |
| Antigravity CLI | native default creation | `--conversation <id>` | `~/.gemini/antigravity-cli/conversations/<id>.db` metadata plus lifecycle-hook capture |
An explicit native selector such as Claude's `--resume`, OpenCode's `--session`,
or Codex's `resume` wins. ai-memory links the selected native session and resets
an unrelated adapter cursor rather than assuming it belongs to the old session.
Codex's `resume`, or Antigravity's `--conversation` / `--continue` wins.
ai-memory links the selected native session and resets an unrelated adapter
cursor rather than assuming it belongs to the old session.
Pi and OMP `--session-dir` values and Crush `--data-dir` values are passed
through unchanged and used as the read-only import root. Native store
environment overrides are also honored:
@@ -180,8 +182,9 @@ chooser.
the harness's native dangerous mode. The translation is Claude Code
`--dangerously-skip-permissions`, Codex
`--dangerously-bypass-approvals-and-sandbox`, OpenCode `--auto`, Pi `--approve`,
Crush `--yolo`, Kimi Code `--yolo`, and Grok Build CLI `--yolo` (equivalent to
its `--always-approve` option). OMP currently needs no added flag. ai-memory
Crush `--yolo`, Kimi Code `--yolo`, Grok Build CLI `--yolo` (equivalent to its
`--always-approve` option), and Antigravity CLI
`--dangerously-skip-permissions`. OMP currently needs no added flag. ai-memory
does not add a duplicate when the translated native flag is already present.
Managed support is intentionally narrower than the general integration matrix.
@@ -260,15 +263,16 @@ workspace is the directory `agy` was launched from, not a checkout root, so a
conversation started one level up is not offered inside a subdirectory.
`agy` accepts no caller-chosen id for a new conversation, so a fresh launch
injects nothing and the id is linked by the hooks or discovered after exit; a
injects no selector and the id is linked by the hooks or discovered after exit; a
linked resume passes `--conversation <id>`. `--continue` / `-c` is treated as an
explicit user choice and is never overridden. `--yolo` maps to
`--dangerously-skip-permissions`. Step payloads are undocumented, unversioned
protobuf blobs, so ai-memory does not decode conversation text: the visible-event
ledger for this harness comes from lifecycle-hook capture, and transcript export
fails with a message saying so. The managed launcher accepts `antigravity`,
`antigravity-cli`, and `agy`. Antigravity is not part of the no-argument
auto-detection set; name it explicitly or pick it from `ai-memory show`.
`antigravity-cli`, and `agy`. The native contract was verified against
Antigravity CLI v1.1.7. Antigravity is not part of the no-argument
auto-detection set; name it explicitly.
Crush needs no ai-memory hook installation for managed mode. The launcher reads
its one-time context from the server, copies the existing global Crush JSON into
@@ -322,10 +326,10 @@ process launch is fatal; ai-memory does not silently start an unmanaged agent.
## Privacy and storage boundaries
ai-memory's managed adapters do not write to Claude, Codex, OpenCode, Pi, Crush,
Kimi Code, OMP, or Grok private stores. The launched harness retains normal
ownership of its own session writes. Adapters read only documented or observed
local session formats. Provider credentials, encrypted content,
system/developer prompt records, and hidden reasoning are not copied. The
Kimi Code, OMP, Grok, or Antigravity private stores. The launched harness
retains normal ownership of its own session writes. Adapters read only
documented or observed local session formats. Provider credentials, encrypted
content, system/developer prompt records, and hidden reasoning are not copied. The
server sanitizer runs before both the SQLite FTS ledger and immutable files under
`<data_dir>/raw/workstreams/<workstream-id>/segments/` are written.
@@ -339,8 +343,9 @@ belong in wiki pages through consolidation or explicit durable writes.
project name. Wiki paths are UUID-keyed, so it moves no server directory, source
checkout, or native harness session. If the source checkout path itself is
renamed, absolute-path session locators used by Claude Code, Codex, OpenCode,
Pi, Kimi Code (`state.json`'s `workDir`), and OMP may still reference the old
path; Crush's project-local `.crush` database moves with the checkout.
Pi, Kimi Code (`state.json`'s `workDir`), OMP, and Antigravity may still
reference the old path; Crush's project-local `.crush` database moves with the
checkout.
There is no portable, supported API that rewrites every harness's private
project locator. ai-memory therefore does not mutate those stores or silently
@@ -369,12 +374,15 @@ OpenCode receive only copied authentication material; OMP receives a temporary
agent directory with read-consistent credential/model database backups and
copied settings. Crush uses its existing global provider configuration and an
isolated project database. Kimi Code runs with an isolated `$KIMI_CODE_HOME`
seeded with the operator's provider configuration. The deterministic phase
also covers first-run adoption, bare-mode selection and empty-directory
failure, wrapper `--yolo`, lease exclusion, Crush context cleanup, a fake-mode
Kimi store/resume/import round trip, and the established-workstream guard
against obsolete sessions. The fake Kimi round trip also deletes the linked
native session and verifies automatic fresh-session recovery and repointing.
seeded with the operator's provider configuration. Antigravity runs with an
isolated `HOME` seeded only with the operator's OAuth and settings files. The
deterministic phase also covers first-run adoption, bare-mode selection and
empty-directory failure, wrapper `--yolo`, lease exclusion, Crush context
cleanup, a fake-mode Kimi store/resume/import round trip, an Antigravity
hook/link/resume round trip, private-trajectory exclusion, and the
established-workstream guard against obsolete sessions. The fake Kimi round
trip also deletes the linked native session and verifies automatic
fresh-session recovery and repointing.
Native session creation, read-only extraction, cross-harness injection, and
returning resume paths are all exercised. Docker wrapper host execution and
remote URL preservation are covered separately by the `ai-memory-cli`
@@ -382,19 +390,22 @@ packaging tests.
The real-harness phase treats the model as the system under transport, not as
the test oracle. For each leg it records the prior ledger sequence, then
requires a newly imported assistant event from that harness. When a context
delta is expected, it first verifies that the prior ledger endpoint is newer
requires a newly imported assistant event from harnesses with readable native
transcripts. For Antigravity it instead requires the exact native conversation
link and a new correlated startup-hook observation, because its private
trajectory protobuf is deliberately not decoded. When a context delta is
expected, it first verifies that the prior ledger endpoint is newer
than that harness's delivery cursor, then requires the latest managed run to
report that exact endpoint as `sync_through` with `context_delivered = 1`. It
does not require the model to quote a prior sentinel: Claude Code may
externalize a large hook result to a file, and whether a model chooses to read
that file is not a deterministic continuity signal. The deterministic fake
Grok cross-harness fixture exercises the same assertion helper without
credentials or model calls.
Grok and Antigravity cross-harness fixtures exercise the same assertion helper
without credentials or model calls.
Set
`AI_MEMORY_ACCEPTANCE_HARNESSES="kimi-cli codex"` to select a
Kimi-to-Codex-to-Kimi round trip (Kimi aliases normalize to the installed
`kimi` executable), `AI_MEMORY_ACCEPTANCE_DETERMINISTIC_ONLY=1` to skip model
`AI_MEMORY_ACCEPTANCE_HARNESSES="antigravity codex"` to select an
Antigravity-to-Codex-to-Antigravity round trip (`agy` and `antigravity-cli` are
accepted aliases), `AI_MEMORY_ACCEPTANCE_DETERMINISTIC_ONLY=1` to skip model
calls, or
`AI_MEMORY_ACCEPTANCE_KEEP=1` to retain all temporary logs and data.
+4 -4
View File
@@ -6,8 +6,8 @@ agent CLI actually runs.
## Rule Of Thumb
Run `install-mcp` and `install-hooks` from the same environment that
launches Claude Code, Codex, Devin CLI, Cursor, Gemini CLI, Kimi Code, or
another agent.
launches Claude Code, Codex, Devin CLI, Cursor, Gemini CLI, Kimi Code,
Antigravity CLI, or another agent.
- If the agent runs inside WSL2, install ai-memory inside WSL2.
- If the agent runs as a native Windows process, install ai-memory from
@@ -347,8 +347,8 @@ Windows agent builds.
Claude Code invokes hooks as a direct binary call (no shell) by default;
`AI_MEMORY_HOOK_PLATFORM=windows-bash` restores the Git Bash `bash -c`
path. WSL2 Claude Code uses normal WSL `.sh` paths.
- Codex, Devin CLI, OpenCode, Cursor, Gemini CLI, Grok Build CLI, Zero,
Kimi Code, and OpenClaw may each choose different
- Codex, Devin CLI, OpenCode, Cursor, Gemini CLI, Antigravity CLI, Grok Build
CLI, Zero, Kimi Code, and OpenClaw may each choose different
Windows config locations or shell execution behavior. ai-memory uses
the current best-known defaults, but they need validation on real
installations.
+230 -21
View File
@@ -8,7 +8,7 @@ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
BIN=${AI_MEMORY_ACCEPTANCE_BIN:-"$ROOT/target/debug/ai-memory"}
KEEP=${AI_MEMORY_ACCEPTANCE_KEEP:-0}
DETERMINISTIC_ONLY=${AI_MEMORY_ACCEPTANCE_DETERMINISTIC_ONLY:-0}
HARNESS_WORDS=${AI_MEMORY_ACCEPTANCE_HARNESSES:-"claude codex opencode pi crush omp kimi grok"}
HARNESS_WORDS=${AI_MEMORY_ACCEPTANCE_HARNESSES:-"claude codex opencode pi crush omp kimi grok antigravity"}
TMP=$(mktemp -d "${TMPDIR:-/tmp}/ai-memory-workstream-acceptance.XXXXXX")
DATA="$TMP/data"
REPO="$TMP/repo"
@@ -109,9 +109,21 @@ current_delivery_cursor() {
), 0);"
}
agent_observation_count() {
local agent=$1
sqlite3 "$DATA/db/memory.sqlite" \
"SELECT COUNT(*)
FROM observations AS o
JOIN sessions AS s ON s.id = o.session_id
WHERE s.agent_kind = '$agent';"
}
# Assert the deterministic boundaries of one real or fake managed leg:
# the harness produced a new assistant event, and any assigned context delta
# was acknowledged by its hook/launcher delivery path. Model recall is not a
# the harness produced new portable evidence, and any assigned context delta
# was acknowledged by its hook/launcher delivery path. Adapters with readable
# transcripts persist an assistant event. Antigravity deliberately does not
# decode private trajectory protobuf, so its startup hook must instead persist
# an observation and link the exact native conversation. Model recall is not a
# protocol oracle: large hook results may be file-backed, and whether a model
# chooses to read that file must not decide acceptance.
assert_managed_leg() {
@@ -121,21 +133,42 @@ assert_managed_leg() {
local before_delivery=$4
local expect_context=$5
local log=$6
local native_id delivery sync_after sync_through context_delivered
local before_observations=${7:-0}
local native_id delivery sync_after sync_through context_delivered after_observations
native_id=$(sqlite3 "$DATA/db/memory.sqlite" \
"SELECT native_session_id FROM workstream_events
WHERE workstream_id = x'$hex'
AND sequence > $before_sequence
AND agent_kind = '$expected_agent'
AND role = 'assistant'
ORDER BY sequence DESC LIMIT 1;")
[ -n "$native_id" ] || {
printf '%s did not persist a new assistant event after ledger sequence %s\n' \
"$expected_agent" "$before_sequence" >&2
tail -120 "$log" >&2
return 1
}
if [ "$expected_agent" = antigravity-cli ]; then
native_id=$(sqlite3 "$DATA/db/memory.sqlite" \
"SELECT native_session_id FROM workstream_native_sessions
WHERE workstream_id = x'$hex'
AND agent_kind = '$expected_agent'
AND is_current = 1;")
[ -n "$native_id" ] || {
printf '%s did not link a native conversation\n' "$expected_agent" >&2
tail -120 "$log" >&2
return 1
}
after_observations=$(agent_observation_count "$expected_agent")
[ "$after_observations" -gt "$before_observations" ] || {
printf '%s did not persist a startup-hook observation (before: %s, after: %s)\n' \
"$expected_agent" "$before_observations" "$after_observations" >&2
tail -120 "$log" >&2
return 1
}
else
native_id=$(sqlite3 "$DATA/db/memory.sqlite" \
"SELECT native_session_id FROM workstream_events
WHERE workstream_id = x'$hex'
AND sequence > $before_sequence
AND agent_kind = '$expected_agent'
AND role = 'assistant'
ORDER BY sequence DESC LIMIT 1;")
[ -n "$native_id" ] || {
printf '%s did not persist a new assistant event after ledger sequence %s\n' \
"$expected_agent" "$before_sequence" >&2
tail -120 "$log" >&2
return 1
}
fi
if [ "$expect_context" = 1 ]; then
if [ "$before_sequence" -le "$before_delivery" ]; then
@@ -242,6 +275,42 @@ case "${AI_MEMORY_ACCEPTANCE_FAKE_MODE:-argv}" in
printf '{"type":"user","content":[{"type":"text","text":"%s"}]}\n' "$sentinel" >>"$chat"
printf '{"type":"assistant","content":"%s reply"}\n' "$sentinel" >>"$chat"
;;
antigravity)
printf '%s\n' "$@" >"$AI_MEMORY_ACCEPTANCE_ARGV_LOG"
session_id=""
previous_arg=""
for arg in "$@"; do
if [ "$previous_arg" = --conversation ]; then
session_id=$arg
fi
previous_arg=$arg
done
if [ -z "$session_id" ]; then
session_id=${AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_SESSION_ID:?antigravity fake mode requires a fresh session id}
fi
conversations="$HOME/.gemini/antigravity-cli/conversations"
mkdir -p "$conversations"
database="$conversations/$session_id.db"
if [ ! -f "$database" ]; then
uri="file://$PWD"
uri_hex=$(printf '%s' "$uri" | od -An -tx1 | tr -d ' \n')
printf -v uri_length '%02x' "${#uri}"
nested="0a${uri_length}${uri_hex}"
printf -v nested_length '%02x' "$((2 + ${#uri}))"
metadata="0a${nested_length}${nested}"
sqlite3 "$database" \
"CREATE TABLE trajectory_metadata_blob (id text DEFAULT 'main', data blob, PRIMARY KEY (id));
INSERT INTO trajectory_metadata_blob (id, data) VALUES ('main', x'$metadata');
CREATE TABLE trajectory_blob (data blob);
INSERT INTO trajectory_blob (data) VALUES (x'414d57532d505249564154452d5452414a4543544f5259');"
fi
payload=$(jq -nc --arg id "$session_id" --arg cwd "$PWD" \
'{invocationNum: 0, conversationId: $id, workspacePaths: [$cwd]}')
printf '%s' "$payload" | \
AI_MEMORY_HOOK_URL="${AI_MEMORY_SERVER_URL:?}" \
"$AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK" \
>"$AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK_LOG"
;;
esac
EOF
chmod +x "$FAKE"
@@ -551,6 +620,106 @@ kimi_current_id=$(sqlite3 "$DATA/db/memory.sqlite" \
exit 1
}
# Antigravity fake-mode fixture: `agy` owns the conversation id and SQLite
# database, while its real PreInvocation hook links that id and accepts any
# pending workstream context. The private trajectory table is never decoded or
# copied into the ledger.
ANTIGRAVITY_FAKE_HOME="$CONFIG/antigravity-fake"
ANTIGRAVITY_FAKE_ID="a0d5ac62-2501-4780-b783-76d159c56cb3"
ANTIGRAVITY_HOOK="$ROOT/hooks/antigravity-cli/session-start.sh"
mkdir -p "$ANTIGRAVITY_FAKE_HOME"
antigravity_first_observations=$(agent_observation_count antigravity-cli)
(
cd "$REPO"
HOME="$ANTIGRAVITY_FAKE_HOME" \
AI_MEMORY_ACCEPTANCE_FAKE_MODE=antigravity \
AI_MEMORY_ACCEPTANCE_ARGV_LOG="$TMP/antigravity-first-argv.log" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_SESSION_ID="$ANTIGRAVITY_FAKE_ID" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK="$ANTIGRAVITY_HOOK" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK_LOG="$TMP/antigravity-first-hook.json" \
"$BIN" --data-dir "$DATA" run --new edge-antigravity --executable "$FAKE" \
--yolo antigravity >"$LOGS/edge-antigravity-first.log" 2>&1
)
diff -u <(printf '%s\n' --dangerously-skip-permissions) \
"$TMP/antigravity-first-argv.log"
antigravity_ws_hex=$(workstream_hex edge-antigravity)
[ "${#antigravity_ws_hex}" -eq 32 ] || {
printf 'could not resolve the edge-antigravity workstream id\n' >&2
exit 1
}
antigravity_native=$(assert_managed_leg "$antigravity_ws_hex" antigravity-cli 0 0 0 \
"$LOGS/edge-antigravity-first.log" "$antigravity_first_observations")
[ "$antigravity_native" = "$ANTIGRAVITY_FAKE_ID" ] || {
printf 'Antigravity linked native conversation %s, expected %s\n' \
"$antigravity_native" "$ANTIGRAVITY_FAKE_ID" >&2
exit 1
}
antigravity_ws_id="${antigravity_ws_hex:0:8}-${antigravity_ws_hex:8:4}-${antigravity_ws_hex:12:4}-${antigravity_ws_hex:16:4}-${antigravity_ws_hex:20:12}"
private_hits=$("$BIN" --data-dir "$DATA" workstream-search \
--workstream-id "$antigravity_ws_id" --limit 100 --json "PRIVATE")
jq -e 'length == 0' <<<"$private_hits" >/dev/null || {
printf 'Antigravity private trajectory payload leaked into the workstream ledger\n' >&2
exit 1
}
antigravity_second_before=$(latest_workstream_sequence "$antigravity_ws_hex")
antigravity_second_delivery=$(current_delivery_cursor "$antigravity_ws_hex" antigravity-cli)
antigravity_second_observations=$(agent_observation_count antigravity-cli)
(
cd "$REPO"
HOME="$ANTIGRAVITY_FAKE_HOME" \
AI_MEMORY_ACCEPTANCE_FAKE_MODE=antigravity \
AI_MEMORY_ACCEPTANCE_ARGV_LOG="$TMP/antigravity-second-argv.log" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_SESSION_ID="$ANTIGRAVITY_FAKE_ID" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK="$ANTIGRAVITY_HOOK" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK_LOG="$TMP/antigravity-second-hook.json" \
"$BIN" --data-dir "$DATA" run --workstream edge-antigravity \
--executable "$FAKE" agy >"$LOGS/edge-antigravity-second.log" 2>&1
)
diff -u <(printf '%s\n' --conversation "$ANTIGRAVITY_FAKE_ID") \
"$TMP/antigravity-second-argv.log"
returned_antigravity=$(assert_managed_leg "$antigravity_ws_hex" antigravity-cli \
"$antigravity_second_before" "$antigravity_second_delivery" 0 \
"$LOGS/edge-antigravity-second.log" "$antigravity_second_observations")
[ "$returned_antigravity" = "$ANTIGRAVITY_FAKE_ID" ] || {
printf 'returning Antigravity launch did not resume %s\n' "$ANTIGRAVITY_FAKE_ID" >&2
exit 1
}
# A fresh Antigravity conversation joining Kimi's established workstream must
# receive the prior visible ledger through the real hook's injectSteps output.
ANTIGRAVITY_CROSS_HOME="$CONFIG/antigravity-cross"
ANTIGRAVITY_CROSS_ID="9576275f-7c4e-4709-b372-22d1ad2a0af8"
mkdir -p "$ANTIGRAVITY_CROSS_HOME"
antigravity_cross_before=$(latest_workstream_sequence "$kimi_ws_hex")
antigravity_cross_delivery=$(current_delivery_cursor "$kimi_ws_hex" antigravity-cli)
antigravity_cross_observations=$(agent_observation_count antigravity-cli)
(
cd "$REPO"
HOME="$ANTIGRAVITY_CROSS_HOME" \
AI_MEMORY_ACCEPTANCE_FAKE_MODE=antigravity \
AI_MEMORY_ACCEPTANCE_ARGV_LOG="$TMP/antigravity-cross-argv.log" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_SESSION_ID="$ANTIGRAVITY_CROSS_ID" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK="$ANTIGRAVITY_HOOK" \
AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_HOOK_LOG="$TMP/antigravity-cross-hook.json" \
"$BIN" --data-dir "$DATA" run --workstream edge-kimi --executable "$FAKE" \
antigravity-cli >"$LOGS/edge-antigravity-cross.log" 2>&1
)
if grep -q . "$TMP/antigravity-cross-argv.log"; then
printf 'fresh Antigravity launch unexpectedly received a session selector\n' >&2
cat "$TMP/antigravity-cross-argv.log" >&2
exit 1
fi
jq -e '.injectSteps[0].ephemeralMessage | contains("AMWS-FAKE-KIMI")' \
"$TMP/antigravity-cross-hook.json" >/dev/null || {
printf 'Antigravity hook did not receive the prior Kimi workstream history\n' >&2
cat "$TMP/antigravity-cross-hook.json" >&2
exit 1
}
assert_managed_leg "$kimi_ws_hex" antigravity-cli "$antigravity_cross_before" \
"$antigravity_cross_delivery" 1 "$LOGS/edge-antigravity-cross.log" \
"$antigravity_cross_observations" >/dev/null
# Grok fake-mode fixture: the fake grok honors the wrapper's `--session-id`
# (fresh) and `--resume <id>` (returning) selectors, writes the native store
# layout ($GROK_HOME/sessions/<bucket>/<id>/summary.json plus
@@ -736,9 +905,12 @@ for requested_harness in "${requested_harnesses[@]}"; do
case "$requested_harness" in
kimi-code | kimi-cli) harness=kimi ;;
grok-build) harness=grok ;;
antigravity-cli | agy) harness=antigravity ;;
*) harness=$requested_harness ;;
esac
if command -v "$harness" >/dev/null 2>&1; then
harness_command=$harness
[ "$harness" != antigravity ] || harness_command=agy
if command -v "$harness_command" >/dev/null 2>&1; then
harnesses+=("$harness")
else
printf 'skipping unavailable harness: %s\n' "$requested_harness" >&2
@@ -762,10 +934,14 @@ OMP_AGENT_DIR="$CONFIG/omp/agent"
CRUSH_DATA_DIR="$CONFIG/crush/data"
KIMI_ACCEPTANCE_HOME="$CONFIG/kimi-home"
GROK_ACCEPTANCE_HOME="$CONFIG/grok-home"
ANTIGRAVITY_ACCEPTANCE_HOME="$CONFIG/antigravity-home"
ANTIGRAVITY_HOOKS="$ANTIGRAVITY_ACCEPTANCE_HOME/.gemini/config/hooks.json"
mkdir -p "$(dirname "$CLAUDE_SETTINGS")" "$(dirname "$CODEX_HOOKS")" \
"$(dirname "$OPENCODE_PLUGIN")" "$(dirname "$PI_EXTENSION")" \
"$(dirname "$OMP_EXTENSION")" "$OMP_AGENT_DIR" "$OPENCODE_DATA_HOME/opencode" \
"$CRUSH_DATA_DIR" "$KIMI_ACCEPTANCE_HOME" "$GROK_ACCEPTANCE_HOME"
"$CRUSH_DATA_DIR" "$KIMI_ACCEPTANCE_HOME" "$GROK_ACCEPTANCE_HOME" \
"$(dirname "$ANTIGRAVITY_HOOKS")" \
"$ANTIGRAVITY_ACCEPTANCE_HOME/.gemini/antigravity-cli"
# Redirect native transcript stores into the fixture while reusing only the
# minimum authentication material required for real model calls.
@@ -829,6 +1005,26 @@ for relative in auth.json config.toml; do
fi
done
# Antigravity resolves login, settings, hooks, and conversation databases below
# ~/.gemini. Copy only the minimum OAuth and settings files into an isolated
# HOME; private history, conversation databases, logs, and caches stay out.
for relative in google_accounts.json oauth_creds.json installation_id; do
if [ -f "$HOME/.gemini/$relative" ]; then
cp "$HOME/.gemini/$relative" \
"$ANTIGRAVITY_ACCEPTANCE_HOME/.gemini/$relative"
fi
done
for relative in antigravity-oauth-token installation_id jetski_state.pbtxt settings.json; do
if [ -f "$HOME/.gemini/antigravity-cli/$relative" ]; then
cp "$HOME/.gemini/antigravity-cli/$relative" \
"$ANTIGRAVITY_ACCEPTANCE_HOME/.gemini/antigravity-cli/$relative"
fi
done
if [ -f "$HOME/.gemini/config/config.json" ]; then
cp "$HOME/.gemini/config/config.json" \
"$ANTIGRAVITY_ACCEPTANCE_HOME/.gemini/config/config.json"
fi
install_hook() {
local agent=$1
local target=$2
@@ -838,7 +1034,7 @@ install_hook() {
--config-file "$target"
)
case "$agent" in
claude-code | codex | kimi-code)
claude-code | codex | kimi-code | antigravity-cli)
command+=(--hooks-dir "$ROOT/hooks")
;;
esac
@@ -852,12 +1048,14 @@ install_hook opencode "$OPENCODE_PLUGIN"
install_hook pi "$PI_EXTENSION"
install_hook omp "$OMP_EXTENSION"
install_hook kimi-code "$KIMI_ACCEPTANCE_HOME/config.toml"
install_hook antigravity-cli "$ANTIGRAVITY_HOOKS"
agent_wire_name() {
case "$1" in
claude) printf 'claude-code\n' ;;
opencode) printf 'open-code\n' ;;
kimi) printf 'kimi-code\n' ;;
antigravity) printf 'antigravity-cli\n' ;;
*) printf '%s\n' "$1" ;;
esac
}
@@ -876,9 +1074,11 @@ run_harness() {
local expected_agent
local before_sequence=0
local before_delivery=0
local before_observations=0
local existing_hex
local -a wrapper_args native_args
expected_agent=$(agent_wire_name "$harness")
before_observations=$(agent_observation_count "$expected_agent")
existing_hex=$(workstream_hex "$WORKSTREAM_NAME")
if [ -n "$existing_hex" ]; then
before_sequence=$(latest_workstream_sequence "$existing_hex")
@@ -924,6 +1124,10 @@ run_harness() {
native_args=(-p "$prompt")
[ -z "${AI_MEMORY_ACCEPTANCE_GROK_MODEL:-}" ] || native_args=(-p -m "$AI_MEMORY_ACCEPTANCE_GROK_MODEL" "$prompt")
;;
antigravity)
native_args=(-p --print-timeout "${AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_TIMEOUT:-5m}" "$prompt")
[ -z "${AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_MODEL:-}" ] || native_args=(-p --print-timeout "${AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_TIMEOUT:-5m}" --model "$AI_MEMORY_ACCEPTANCE_ANTIGRAVITY_MODEL" "$prompt")
;;
*)
printf 'unsupported acceptance harness: %s\n' "$harness" >&2
return 1
@@ -957,6 +1161,10 @@ run_harness() {
(cd "$REPO" && GROK_HOME="$GROK_ACCEPTANCE_HOME" \
"$BIN" --data-dir "$DATA" run "${wrapper_args[@]}" "$harness" "${native_args[@]}") \
>"$log" 2>&1
elif [ "$harness" = antigravity ]; then
(cd "$REPO" && HOME="$ANTIGRAVITY_ACCEPTANCE_HOME" \
"$BIN" --data-dir "$DATA" run "${wrapper_args[@]}" "$harness" "${native_args[@]}") \
>"$log" 2>&1
else
(cd "$REPO" && "$BIN" --data-dir "$DATA" run \
"${wrapper_args[@]}" "$harness" "${native_args[@]}") >"$log" 2>&1
@@ -971,7 +1179,8 @@ run_harness() {
return 1
}
if ! native_id=$(assert_managed_leg "$hex" "$expected_agent" \
"$before_sequence" "$before_delivery" "$expect_context" "$log"); then
"$before_sequence" "$before_delivery" "$expect_context" "$log" \
"$before_observations"); then
return 1
fi
printf '%s\n' "$native_id"