mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
Merge branch 'pr-840' into release/2.5
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -33,6 +33,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
Documented producer provenance and stable retries through the existing hook
|
||||
ingestion API, without changing its schema or standalone defaults. (#821)
|
||||
|
||||
### Changed
|
||||
- Grok Build CLI shows a pending handoff, and an opted-in `[briefing]`, as
|
||||
`PostToolUse` `additionalContext` on the first tool of a session.
|
||||
`SessionStart` and `UserPromptSubmit` still do not accept the handoff (Grok
|
||||
discards that stdout); a session that never calls a tool leaves the handoff
|
||||
open for `memory_handoff_accept`. The note is clipped to 10,000 characters
|
||||
(Grok's own cap). Because Grok reuses one session id across a
|
||||
SessionEnd→restart, `memory_handoff_accept` now reopens an already-ended
|
||||
receiver session (clears `ended_at`) instead of rejecting it — but only after
|
||||
the exactly-once claim guard, so a session that already took a baton still
|
||||
cannot take another (the multi-session claim-once invariant is preserved).
|
||||
Delivery of this PostToolUse handoff is exempt from `AI_MEMORY_CAPTURE_OWNER`
|
||||
capture suppression, like the other context-delivery events. (#840)
|
||||
|
||||
### Fixed
|
||||
- `ai-memory serve` no longer leaked file descriptors from half-open HTTP
|
||||
connections until `EMFILE`, breaking the healthcheck (an unauthenticated
|
||||
|
||||
@@ -460,6 +460,55 @@ fn write_success_response<W: std::io::Write>(
|
||||
}
|
||||
}
|
||||
|
||||
const GROK_ADDITIONAL_CONTEXT_CHARS: usize = 10_000;
|
||||
|
||||
fn grok_post_tool_handoff_envelope(handoff: &str) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": clip_chars(handoff, GROK_ADDITIONAL_CONTEXT_CHARS),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn payload_is_subagent(raw: &serde_json::Value) -> bool {
|
||||
[
|
||||
"subagentType",
|
||||
"subagent_type",
|
||||
"agent_type",
|
||||
"agent_id",
|
||||
"parentSessionId",
|
||||
]
|
||||
.iter()
|
||||
.any(|key| {
|
||||
raw.get(*key)
|
||||
.and_then(|value| value.as_str())
|
||||
.is_some_and(|text| !text.trim().is_empty())
|
||||
})
|
||||
}
|
||||
|
||||
fn clip_chars(text: &str, max_chars: usize) -> String {
|
||||
if text.chars().count() <= max_chars {
|
||||
return text.to_string();
|
||||
}
|
||||
let keep = max_chars.saturating_sub(12);
|
||||
let mut out: String = text.chars().take(keep).collect();
|
||||
out.push_str("\n[truncated]");
|
||||
out
|
||||
}
|
||||
|
||||
fn handoff_shown_path(
|
||||
data_dir: &Path,
|
||||
agent: &str,
|
||||
session_id: Option<&str>,
|
||||
cwd: Option<&str>,
|
||||
) -> PathBuf {
|
||||
let key = briefed_marker_path(data_dir, agent, session_id, cwd);
|
||||
data_dir
|
||||
.join("handoff-shown")
|
||||
.join(key.file_name().unwrap_or_default())
|
||||
}
|
||||
|
||||
fn session_start_handoff_envelope(agent: AgentKind, handoff: String) -> serde_json::Value {
|
||||
if agent == AgentKind::AntigravityCli {
|
||||
serde_json::json!({
|
||||
@@ -514,7 +563,11 @@ where
|
||||
let external_capture = env_lookup(CAPTURE_OWNER_ENV).is_some_and(|v| !v.trim().is_empty());
|
||||
let delivers_context = (hook_event == HookEvent::SessionStart
|
||||
&& agent_kind.session_start_injects_handoff())
|
||||
|| (hook_event == HookEvent::UserPrompt && agent_kind.user_prompt_injects_handoff());
|
||||
|| (hook_event == HookEvent::UserPrompt && agent_kind.user_prompt_injects_handoff())
|
||||
// Grok delivers the handoff on the first PostToolUse (its SessionStart /
|
||||
// UserPromptSubmit stdout is discarded); that path is context delivery
|
||||
// too, so external capture must not suppress it.
|
||||
|| (hook_event == HookEvent::PostToolUse && agent_kind.post_tool_injects_handoff());
|
||||
if external_capture && !args.check_capture && !delivers_context {
|
||||
// Retiring the fallback session ID is lifecycle housekeeping, not
|
||||
// capture. Preserve it even when no event is enqueued.
|
||||
@@ -789,12 +842,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// user-prompt: agents whose SessionStart stdout is discarded (Kimi Code)
|
||||
// receive the handoff here instead — kimi injects UserPromptSubmit stdout
|
||||
// into the turn verbatim as a `hook_result` user message. The payload
|
||||
// carries the native session id when available, so the destructive GET
|
||||
// can also link the managed run to the native session, same as
|
||||
// session-start does.
|
||||
// user-prompt: agents whose SessionStart stdout is discarded AND whose
|
||||
// UserPromptSubmit stdout is injected (Kimi Code) receive the handoff
|
||||
// here. Grok discards both, so it must not take this path: the GET
|
||||
// accepts the handoff. The payload carries the native session id when
|
||||
// available, so the destructive GET can also link the managed run to the
|
||||
// native session, same as session-start does.
|
||||
// The installed kimi hook passes the script stem (`user-prompt-submit`)
|
||||
// while the legacy shell path posts `user-prompt`; HookEvent::parse
|
||||
// canonicalizes both (and the snake/native spellings) to UserPrompt.
|
||||
@@ -864,6 +917,46 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// post-tool-use: Grok shows hookSpecificOutput.additionalContext to the
|
||||
// model after the tool result. SessionStart and UserPromptSubmit stdout
|
||||
// are discarded, so this is the event that can carry a handoff without
|
||||
// burning it. One fetch per session. A session that never calls a tool
|
||||
// leaves the handoff open for memory_handoff_accept.
|
||||
if HookEvent::parse(&args.event) == HookEvent::PostToolUse
|
||||
&& AgentKind::from_wire(&args.agent).post_tool_injects_handoff()
|
||||
{
|
||||
let shown = handoff_shown_path(
|
||||
&dd,
|
||||
&args.agent,
|
||||
canonical_session_id.as_deref(),
|
||||
policy_cwd.as_deref(),
|
||||
);
|
||||
if !shown.is_file() && !payload_is_subagent(&json) {
|
||||
let client = build_client();
|
||||
let bearer = hook_spool::resolve_bearer(&client, &dd, effective_token).await;
|
||||
let native_session_qs = canonical_session_id
|
||||
.as_deref()
|
||||
.map_or_else(String::new, |session_id| {
|
||||
format!("&session_id={}", url_encode(session_id))
|
||||
});
|
||||
let handoff_url = format!(
|
||||
"{base}/handoff?agent={}{qs}{managed_qs}{native_session_qs}",
|
||||
args.agent
|
||||
);
|
||||
let handoff =
|
||||
get_handoff(&client, &handoff_url, bearer.as_deref(), handoff_timeout()).await;
|
||||
// Only a real body consumes this session's one chance. An empty
|
||||
// or failed claim must retry on the next parent tool; a child
|
||||
// session that errors must not burn the baton for the parent.
|
||||
if let Some(handoff) = handoff {
|
||||
mark_briefed(&shown);
|
||||
let envelope = grok_post_tool_handoff_envelope(&handoff);
|
||||
writeln!(stdout, "{envelope}")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boundary drain trigger: enqueue first, then ask a detached native drainer
|
||||
// to flush the shared spool. `session-end` remains the primary close path,
|
||||
// but `stop` and `pre-compact` also trigger the helper so delivery does not
|
||||
@@ -2452,6 +2545,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn grok_hook_args(event: &str, server_url: &str) -> HookArgs {
|
||||
HookArgs {
|
||||
event: event.into(),
|
||||
agent: "grok".into(),
|
||||
server_url: server_url.into(),
|
||||
auth_token: None,
|
||||
project_strategy: None,
|
||||
check_capture: false,
|
||||
capture_assistant: false,
|
||||
capture_mode: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn kimi_hook_args(event: &str, server_url: &str) -> HookArgs {
|
||||
HookArgs {
|
||||
event: event.into(),
|
||||
@@ -2930,6 +3036,243 @@ mod tests {
|
||||
assert!(!data_dir.join("briefed").exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_session_start_never_fetches_the_handoff() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (base, mut requests) = serve_requests("200 OK", "AMWS-HANDOFF-DELTA").await;
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(tmp.path().to_path_buf()),
|
||||
grok_hook_args("session-start", &base),
|
||||
serde_json::json!({"session_id": "grok-session", "cwd": tmp.path()}).to_string(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(stdout, b"{}\n");
|
||||
while let Some(request) = first_request(&mut requests).await {
|
||||
assert!(!request.starts_with("GET /handoff"), "{request}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_user_prompt_does_not_fetch_the_handoff() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (base, mut requests) = serve_requests("200 OK", "AMWS-HANDOFF-DELTA").await;
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(tmp.path().to_path_buf()),
|
||||
grok_hook_args("user-prompt", &base),
|
||||
serde_json::json!({
|
||||
"session_id": "grok-session",
|
||||
"cwd": tmp.path(),
|
||||
"prompt": "hello"
|
||||
})
|
||||
.to_string(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Grok discards allowing UserPromptSubmit stdout. Fetching would
|
||||
// accept the handoff and then throw the body away.
|
||||
assert_eq!(stdout, b"{}\n");
|
||||
while let Some(request) = first_request(&mut requests).await {
|
||||
assert!(
|
||||
!request.starts_with("GET /handoff"),
|
||||
"grok user-prompt must not accept the handoff: {request}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_user_prompt_submit_stem_does_not_fetch_the_handoff() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let cwd = tmp.path().join("repo");
|
||||
std::fs::create_dir(&cwd).unwrap();
|
||||
write_briefing_marker(&cwd);
|
||||
let (base, mut requests) = serve_requests("200 OK", "AMWS-HANDOFF-DELTA").await;
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(data_dir),
|
||||
grok_hook_args("user-prompt-submit", &base),
|
||||
serde_json::json!({
|
||||
"session_id": "grok-session",
|
||||
"cwd": cwd,
|
||||
"prompt": "hi"
|
||||
})
|
||||
.to_string(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(stdout, b"{}\n");
|
||||
while let Some(request) = first_request(&mut requests).await {
|
||||
assert!(
|
||||
!request.starts_with("GET /handoff"),
|
||||
"briefing opt-in must not make grok fetch /handoff: {request}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_post_tool_prints_additional_context_once() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let (base, mut requests) = serve_requests("200 OK", "AMWS-HANDOFF-DELTA").await;
|
||||
let payload = serde_json::json!({
|
||||
"session_id": "grok-session",
|
||||
"cwd": tmp.path(),
|
||||
"tool_name": "read_file"
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(data_dir.clone()),
|
||||
grok_hook_args("post-tool-use", &base),
|
||||
payload.clone(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let envelope: serde_json::Value = serde_json::from_slice(stdout.trim_ascii()).unwrap();
|
||||
assert_eq!(
|
||||
envelope["hookSpecificOutput"]["hookEventName"],
|
||||
"PostToolUse"
|
||||
);
|
||||
assert_eq!(
|
||||
envelope["hookSpecificOutput"]["additionalContext"],
|
||||
"AMWS-HANDOFF-DELTA"
|
||||
);
|
||||
let first = first_request(&mut requests).await.unwrap();
|
||||
assert!(first.starts_with("GET /handoff?"), "{first}");
|
||||
assert!(first.contains("agent=grok"), "{first}");
|
||||
assert!(first.contains("session_id=grok-session"), "{first}");
|
||||
assert!(
|
||||
data_dir
|
||||
.join("handoff-shown")
|
||||
.join("grok-session")
|
||||
.is_file(),
|
||||
"shown marker missing"
|
||||
);
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(data_dir),
|
||||
grok_hook_args("post-tool-use", &base),
|
||||
payload,
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(stdout, b"{}\n");
|
||||
assert!(
|
||||
first_request(&mut requests).await.is_none(),
|
||||
"second post-tool must not fetch again"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_post_tool_includes_briefing_when_the_marker_opts_in() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let data_dir = tmp.path().join("data");
|
||||
let cwd = tmp.path().join("repo");
|
||||
std::fs::create_dir(&cwd).unwrap();
|
||||
write_briefing_marker(&cwd);
|
||||
let (base, mut requests) = serve_requests("200 OK", "AMWS-HANDOFF-DELTA").await;
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(data_dir),
|
||||
grok_hook_args("post-tool-use", &base),
|
||||
serde_json::json!({
|
||||
"session_id": "grok-session",
|
||||
"cwd": cwd,
|
||||
"tool_name": "read_file"
|
||||
})
|
||||
.to_string(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let request = first_request(&mut requests).await.unwrap();
|
||||
assert!(request.contains("&briefing=true"), "{request}");
|
||||
assert!(request.contains("&briefing_budget=6000"), "{request}");
|
||||
let envelope: serde_json::Value = serde_json::from_slice(stdout.trim_ascii()).unwrap();
|
||||
assert_eq!(
|
||||
envelope["hookSpecificOutput"]["additionalContext"],
|
||||
"AMWS-HANDOFF-DELTA"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_post_tool_skips_a_subagent_payload() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (base, mut requests) = serve_requests("200 OK", "AMWS-HANDOFF-DELTA").await;
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(tmp.path().join("data")),
|
||||
grok_hook_args("post-tool-use", &base),
|
||||
serde_json::json!({
|
||||
"session_id": "child-session",
|
||||
"cwd": tmp.path(),
|
||||
"subagentType": "goal-plan-writer",
|
||||
"tool_name": "read_file"
|
||||
})
|
||||
.to_string(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(stdout, b"{}\n");
|
||||
while let Some(request) = first_request(&mut requests).await {
|
||||
assert!(
|
||||
!request.starts_with("GET /handoff"),
|
||||
"a child session must not accept the parent handoff: {request}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grok_post_tool_without_handoff_prints_empty_object() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (base, mut requests) = serve_requests("404 Not Found", "").await;
|
||||
let mut stdout = Vec::new();
|
||||
run_with_payload(
|
||||
Some(tmp.path().join("data")),
|
||||
grok_hook_args("post-tool-use", &base),
|
||||
serde_json::json!({"session_id": "grok-session", "cwd": tmp.path()}).to_string(),
|
||||
&mut stdout,
|
||||
|_, _| Ok(()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(stdout, b"{}\n");
|
||||
let request = first_request(&mut requests).await.unwrap();
|
||||
assert!(request.starts_with("GET /handoff?"), "{request}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grok_additional_context_is_clipped_to_the_model_cap() {
|
||||
let long = "x".repeat(GROK_ADDITIONAL_CONTEXT_CHARS + 50);
|
||||
let envelope = grok_post_tool_handoff_envelope(&long);
|
||||
let note = envelope["hookSpecificOutput"]["additionalContext"]
|
||||
.as_str()
|
||||
.unwrap();
|
||||
assert!(note.ends_with("\n[truncated]"));
|
||||
assert!(note.chars().count() <= GROK_ADDITIONAL_CONTEXT_CHARS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn briefed_markers_are_bounded_and_keep_current() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -362,8 +362,10 @@ impl AgentKind {
|
||||
/// into the resuming session as context. Agents that consume it return
|
||||
/// `true` (Claude Code reads `hookSpecificOutput.additionalContext`).
|
||||
///
|
||||
/// Grok ignores hook stdout on `SessionStart` (per Grok's hooks docs:
|
||||
/// "For events like SessionStart or PostToolUse, stdout is ignored"), and
|
||||
/// Grok ignores hook stdout on `SessionStart` (per Grok's hooks guide:
|
||||
/// stdout for that event is ignored). `PostToolUse` stdout is read and
|
||||
/// `additionalContext` is shown to the model after the tool result. See
|
||||
/// [`Self::post_tool_injects_handoff`].
|
||||
/// Zero's agent loop discards the sessionStart dispatch result entirely
|
||||
/// (`internal/agent/loop.go` ignores `Dispatch`'s return there), so
|
||||
/// the native hook must NOT fetch the handoff for it: the fetch is
|
||||
@@ -414,10 +416,31 @@ impl AgentKind {
|
||||
/// `session/hooks/user-prompt.ts`, verified in the v0.28.1 source).
|
||||
/// Empty stdout injects nothing, so the hook prints the raw handoff body
|
||||
/// or nothing at all — never a JSON envelope.
|
||||
///
|
||||
/// Grok Build also ignores `SessionStart` stdout, and it is not in this
|
||||
/// set. An allowing `UserPromptSubmit` discards stdout and has no
|
||||
/// `additionalContext`. `GET /handoff` marks the handoff accepted, so
|
||||
/// fetching on that event would burn the baton. Grok shows
|
||||
/// `PostToolUse` `additionalContext` to the model; that is
|
||||
/// [`Self::post_tool_injects_handoff`].
|
||||
#[must_use]
|
||||
pub fn user_prompt_injects_handoff(self) -> bool {
|
||||
matches!(self, Self::KimiCode)
|
||||
}
|
||||
|
||||
/// Whether `PostToolUse` stdout is model-visible context.
|
||||
///
|
||||
/// Grok Build reads that stdout and delivers `hookSpecificOutput.additionalContext`
|
||||
/// after the tool result (`10-hooks.md`, PostToolUse Output). The handoff
|
||||
/// is accepted on the first such event of a session, not on `SessionStart`
|
||||
/// or `UserPromptSubmit`, because those outputs never reach the model.
|
||||
/// The model sees the handoff after the first tool, not before the first
|
||||
/// prompt. A session that never calls a tool leaves the handoff open for
|
||||
/// `memory_handoff_accept`.
|
||||
#[must_use]
|
||||
pub fn post_tool_injects_handoff(self) -> bool {
|
||||
matches!(self, Self::Grok)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -457,9 +480,13 @@ mod tests {
|
||||
);
|
||||
// Unknown tags still degrade to Other.
|
||||
assert_eq!(AgentKind::from_wire("grok-2"), AgentKind::Other);
|
||||
// Grok cannot inject the session-start handoff (ignores hook stdout);
|
||||
// every other agent can.
|
||||
// Grok cannot inject the session-start handoff (ignores hook stdout),
|
||||
// and must not fetch on UserPromptSubmit either (that stdout is discarded).
|
||||
assert!(!AgentKind::Grok.session_start_injects_handoff());
|
||||
assert!(!AgentKind::Grok.user_prompt_injects_handoff());
|
||||
assert!(AgentKind::Grok.post_tool_injects_handoff());
|
||||
assert!(!AgentKind::KimiCode.post_tool_injects_handoff());
|
||||
assert!(!AgentKind::ClaudeCode.post_tool_injects_handoff());
|
||||
assert!(!AgentKind::Zero.session_start_injects_handoff());
|
||||
assert!(AgentKind::ClaudeCode.session_start_injects_handoff());
|
||||
assert!(AgentKind::Codex.session_start_injects_handoff());
|
||||
|
||||
@@ -2945,11 +2945,6 @@ pub(crate) fn accept_handoff_in_transaction(
|
||||
"handoff receiver session does not match the accepting scope and agent".into(),
|
||||
));
|
||||
}
|
||||
if !open {
|
||||
return Err(StoreError::InvalidState(
|
||||
"an ended session cannot accept a handoff".into(),
|
||||
));
|
||||
}
|
||||
let already_claimed: bool = tx.query_row(
|
||||
"SELECT EXISTS( \
|
||||
SELECT 1 FROM handoffs \
|
||||
@@ -2964,6 +2959,15 @@ pub(crate) fn accept_handoff_in_transaction(
|
||||
// the first accepted row.
|
||||
return Ok(false);
|
||||
}
|
||||
if !open {
|
||||
// Grok reuses the session id after SessionEnd when the same
|
||||
// conversation restarts. The row is the receiver, not a corpse,
|
||||
// as long as it has not already taken a baton.
|
||||
tx.execute(
|
||||
"UPDATE sessions SET ended_at = NULL WHERE id = ?1",
|
||||
params![accepting_session.as_bytes()],
|
||||
)?;
|
||||
}
|
||||
}
|
||||
let metadata = tx
|
||||
.query_row(
|
||||
|
||||
@@ -358,3 +358,94 @@ async fn an_owned_handoff_stays_with_its_owner_while_pages_stay_shared() {
|
||||
operator("alice")
|
||||
);
|
||||
}
|
||||
|
||||
/// Grok reuses one session id across a SessionEnd→restart, so
|
||||
/// `accept_handoff` must reopen an already-ended receiver session instead of
|
||||
/// rejecting it (#840) — but only *after* the exactly-once claim guard, so the
|
||||
/// resurrection can never become a way to steal an already-taken baton.
|
||||
#[tokio::test]
|
||||
async fn accept_reopens_an_ended_receiver_session_but_keeps_claim_once() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Store::open(tmp.path()).unwrap();
|
||||
let (ws, proj) = scope(&store).await;
|
||||
|
||||
let id = store
|
||||
.writer
|
||||
.insert_handoff(NewHandoff {
|
||||
workspace_id: ws,
|
||||
project_id: proj,
|
||||
from_agent: AgentKind::Grok,
|
||||
to_agent: None,
|
||||
from_session_id: None,
|
||||
summary: "resume after restart".into(),
|
||||
next_steps: Vec::new(),
|
||||
open_questions: Vec::new(),
|
||||
files_touched: Vec::new(),
|
||||
cwd: None,
|
||||
owner_user: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let accept = |session: SessionId| HandoffAcceptance {
|
||||
handoff_id: id,
|
||||
workspace_id: ws,
|
||||
project_id: proj,
|
||||
accepting_agent: AgentKind::Grok,
|
||||
accepting_session: Some(session),
|
||||
accepting_user: None,
|
||||
owner_filter: OwnerFilter::Any,
|
||||
receiving_cwd: None,
|
||||
};
|
||||
|
||||
// The same Grok session id: opened, then ended (SessionEnd), then reused
|
||||
// when the conversation restarts and calls its first tool.
|
||||
let grok = open_session(&store, ws, proj, AgentKind::Grok).await;
|
||||
store.writer.end_session(grok, None).await.unwrap();
|
||||
|
||||
let claimed = store.writer.accept_handoff(accept(grok)).await.unwrap();
|
||||
assert!(
|
||||
claimed,
|
||||
"an ended session that reuses its id must be able to accept the handoff"
|
||||
);
|
||||
|
||||
// The receiver row was reopened (ended_at cleared), not left a corpse.
|
||||
let grok_bytes = grok.as_bytes().to_vec();
|
||||
let ended_at: Option<i64> = store
|
||||
.reader
|
||||
.with_conn(move |conn| {
|
||||
Ok(conn.query_row(
|
||||
"SELECT ended_at FROM sessions WHERE id = ?1",
|
||||
rusqlite::params![grok_bytes],
|
||||
|r| r.get(0),
|
||||
)?)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
ended_at.is_none(),
|
||||
"accepting a handoff must reopen the ended receiver session"
|
||||
);
|
||||
|
||||
// Claim-once still holds: a *different* session cannot steal the baton the
|
||||
// reopened session already took, even though the loser is wide open.
|
||||
let loser = open_session(&store, ws, proj, AgentKind::Codex).await;
|
||||
let stolen = store
|
||||
.writer
|
||||
.accept_handoff(HandoffAcceptance {
|
||||
handoff_id: id,
|
||||
workspace_id: ws,
|
||||
project_id: proj,
|
||||
accepting_agent: AgentKind::Codex,
|
||||
accepting_session: Some(loser),
|
||||
accepting_user: None,
|
||||
owner_filter: OwnerFilter::Any,
|
||||
receiving_cwd: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
!stolen,
|
||||
"the resurrection path must not let a second session steal an accepted baton"
|
||||
);
|
||||
}
|
||||
|
||||
+8
-3
@@ -1437,14 +1437,19 @@ Cursor, Gemini CLI, Antigravity CLI, Grok Build CLI, Kiro CLI, Command Code, and
|
||||
`$GROK_HOME/config.toml` (default `~/.grok/config.toml`); its hooks live under
|
||||
`$GROK_HOME/hooks` (default `~/.grok/hooks`). `install-hooks --agent grok`
|
||||
captures lifecycle events.
|
||||
Grok ignores `SessionStart` stdout, so handoffs must be accepted through MCP with
|
||||
`memory_handoff_accept` when resuming. Claude Desktop, VS Code Copilot, Zed,
|
||||
Grok ignores `SessionStart` stdout and discards an allowing `UserPromptSubmit`,
|
||||
so those hooks do not accept the handoff. The first `PostToolUse` prints
|
||||
`hookSpecificOutput.additionalContext` (pending handoff, plus an opted-in
|
||||
`[briefing]`). The model sees it after that tool result, not before the
|
||||
first prompt. A session with no tool call leaves the handoff for
|
||||
`memory_handoff_accept`. Claude Desktop, VS Code Copilot, Zed,
|
||||
and ZCode
|
||||
are MCP-only here, so you'll need to nudge the model to call
|
||||
`memory_query` / `memory_handoff_accept` itself.
|
||||
For clients with `install-hooks` support, the capture path handles
|
||||
handoff injection at session start or the client's closest equivalent, except
|
||||
for Grok's (and Zero's) no-stdout SessionStart behavior (Antigravity CLI uses `PreInvocation`).
|
||||
for Zero's no-stdout SessionStart behavior. Grok delivers on the first
|
||||
`PostToolUse` instead (Antigravity CLI uses `PreInvocation`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+11
-8
@@ -47,7 +47,7 @@ endpoint. The trade-off:
|
||||
| | What you get | What you don't get |
|
||||
|---|---|---|
|
||||
| **MCP only** | LLM can query the wiki, accept handoffs, run memory_consolidate, and run `memory_auto_improve` learning reviews | No automatic session-end summaries; no auto-handoff at session boundaries |
|
||||
| **MCP + hooks** | All of the above *plus* bounded sanitized prompt/tool-lifecycle observations captured automatically; handoffs surface at SessionStart with no human prompting **only when the client consumes startup-hook output or an equivalent context-injection result** | Hook observations are not complete native transcripts. Grok and Zero discard SessionStart stdout; ask them to call `memory_handoff_accept` when resuming. |
|
||||
| **MCP + hooks** | All of the above *plus* bounded sanitized prompt/tool-lifecycle observations captured automatically; handoffs surface at SessionStart with no human prompting **only when the client consumes startup-hook output or an equivalent context-injection result** | Hook observations are not complete native transcripts. Grok delivers the handoff on the first `PostToolUse`. Zero discards SessionStart stdout; ask it to call `memory_handoff_accept`. |
|
||||
|
||||
For MCP-only use, you can still cover the session-boundary gap by asking
|
||||
the LLM to call `memory_handoff_begin` manually before quitting.
|
||||
@@ -752,8 +752,10 @@ that file and preserves all unrelated MCP servers.
|
||||
## Grok Build CLI
|
||||
|
||||
**Status:** ✅ MCP supported. ✅ Lifecycle hooks supported via
|
||||
`ai-memory install-hooks --agent grok --apply`. ❌ No automatic handoff
|
||||
injection (Grok ignores SessionStart stdout — same policy as Zero).
|
||||
`ai-memory install-hooks --agent grok --apply`. Handoff injection is the
|
||||
first `PostToolUse` (`additionalContext` after the tool result). Grok
|
||||
ignores `SessionStart` stdout and discards an allowing `UserPromptSubmit`,
|
||||
so those events do not accept the handoff.
|
||||
|
||||
**Config file:** `install-mcp --client grok --apply` writes the user config at
|
||||
`$GROK_HOME/config.toml` (default `~/.grok/config.toml`). To use a project or
|
||||
@@ -794,10 +796,11 @@ mirror Claude Code's vocabulary (`SessionStart`, `UserPromptSubmit`,
|
||||
`PreToolUse`, `PostToolUse`, `PreCompact`, `Stop`, `SessionEnd`,
|
||||
`SubagentStart`, `SubagentStop`) with a Grok-specific script bundle /
|
||||
native `ai-memory hook --event … --agent grok` commands. Session-end
|
||||
handoff *creation* works; handoff *injection* does not — ask Grok to
|
||||
call `memory_handoff_accept` (or install the managed routing skills under
|
||||
`.grok/skills` / `$GROK_HOME/skills` (default `~/.grok/skills`)) at the start
|
||||
of a resumed session.
|
||||
handoff *creation* works. Injection is the first `PostToolUse`: JSON
|
||||
`additionalContext` with the pending handoff and an opted-in `[briefing]`.
|
||||
`memory_handoff_accept` is still the path when the session has not called
|
||||
a tool yet (skills live under `.grok/skills` / `$GROK_HOME/skills`, default
|
||||
`~/.grok/skills`).
|
||||
|
||||
Grok can also load MCP from Claude Code / Cursor compat sources when those
|
||||
compat flags are enabled, but first-party `install-mcp --client grok` is
|
||||
@@ -1367,7 +1370,7 @@ that *starts* the next one - to play nicely with ai-memory:
|
||||
| Side | What's needed | Covered by |
|
||||
|---|---|---|
|
||||
| **Ending side** | The agent must create a handoff through a true session-end hook, the manual finalizer, or `memory_handoff_begin`. | Built-in automatically for Claude Code, Codex (native `SessionEnd`, Codex CLI 0.145.0+), Devin CLI, Cursor, Gemini CLI, Grok Build CLI, Zero, Kimi Code, OpenClaw, OpenCode, OpenCode 2 beta, and OMP. Antigravity CLI, both Kiro CLI engines, and Command Code have no reliable true session-end event; run `ai-memory finalize-session` with the corresponding `--agent` after the final turn (also the fallback on Codex older than 0.145.0). MCP-only clients such as Swival must call `memory_handoff_begin` explicitly. |
|
||||
| **Starting side** | Either (a) the session-start/plugin path injects the handoff via `/handoff`, OR (b) the model inspects with `memory_handoff_list` then claims with `memory_handoff_accept` (`handoff_id` from the list). | (a) is built-in for Claude Code / Codex / Devin CLI / Cursor / Gemini CLI / Antigravity CLI / Kimi Code / both Kiro CLI engines / Command Code / OpenClaw / OpenCode / OpenCode 2 beta / OMP. It requires a client that consumes startup-hook stdout or an equivalent context-injection result. Grok and Zero discard SessionStart stdout; Swival is MCP-only. Use (b) for those clients. (b) works for any MCP-capable client if you nudge the model - see [the managed routing package](usage.md#install-the-routing-snippet-and-agent-skills). |
|
||||
| **Starting side** | Either (a) the session-start/plugin path injects the handoff via `/handoff`, OR (b) the model inspects with `memory_handoff_list` then claims with `memory_handoff_accept` (`handoff_id` from the list). | (a) is built-in for Claude Code / Codex / Devin CLI / Cursor / Gemini CLI / Antigravity CLI / Kimi Code / Grok Build CLI / both Kiro CLI engines / Command Code / OpenClaw / OpenCode / OpenCode 2 beta / OMP. Grok's (a) is the first `PostToolUse` `additionalContext`, not SessionStart. Zero discards SessionStart stdout; Swival is MCP-only. Use (b) for those. (b) works for any MCP-capable client if you nudge the model - see [the managed routing package](usage.md#install-the-routing-snippet-and-agent-skills). |
|
||||
|
||||
OpenCode uses its official `session.deleted` plugin event for true session-end
|
||||
delivery. The OpenCode 2 beta plugin subscribes to the same event name on the
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
| Claude Desktop | MCP-only | Uses `mcp-remote`; no lifecycle hooks. |
|
||||
| OpenClaw | Supported | MCP config + native plugin lifecycle hooks; generated plugin enforces capture exclusions. |
|
||||
| Antigravity CLI | Supported | MCP config (`serverUrl`) + lifecycle hooks (`agy` alias). Only `PreInvocation` with `invocationNum = 0` maps to SessionStart; later model calls cannot consume a next-session handoff. No automatic true session-end hook, so run `ai-memory finalize-session --agent antigravity-cli` after the final turn when you need a summary, handoff, and opt-in SessionEnd consolidation. `ai-memory run antigravity` (aliases `antigravity-cli`, `agy`) adds managed workstream resume via `--conversation`; conversation text is not decoded, so the ledger for this harness comes from hook capture. |
|
||||
| Grok Build CLI | Supported | MCP config (`install-mcp --client grok` → `$GROK_HOME/config.toml`, default `~/.grok/config.toml`) + lifecycle hooks (`install-hooks --agent grok` → `$GROK_HOME/hooks/ai-memory.json`, default `~/.grok/hooks/ai-memory.json`, Grok-specific hook bundle). Capture works; no hook handoff injection — Grok ignores `SessionStart` stdout, so recover handoffs via MCP `memory_handoff_list` then `memory_handoff_accept` with that `handoff_id`. `ai-memory run grok` adds managed workstream resume with the context packet delivered natively through `--rules`. Skills root: `.grok/skills` / `$GROK_HOME/skills` (default `~/.grok/skills`). |
|
||||
| Grok Build CLI | Supported | MCP config (`install-mcp --client grok` → `$GROK_HOME/config.toml`, default `~/.grok/config.toml`) + lifecycle hooks (`install-hooks --agent grok` → `$GROK_HOME/hooks/ai-memory.json`, default `~/.grok/hooks/ai-memory.json`, Grok-specific hook bundle). Capture works. `SessionStart` and `UserPromptSubmit` do not accept the handoff (Grok discards that stdout). The first `PostToolUse` prints `hookSpecificOutput.additionalContext` with the pending handoff and an opted-in `[briefing]`; the model sees it after that tool result. If no tool runs, recover via MCP `memory_handoff_list` then `memory_handoff_accept` with that `handoff_id`. `ai-memory run grok` adds managed workstream resume with the context packet delivered natively through `--rules`. Skills root: `.grok/skills` / `$GROK_HOME/skills` (default `~/.grok/skills`). |
|
||||
| Swival CLI | MCP-only | `install-mcp --client swival --apply` merges a native HTTP entry into the project-root `.swival/mcp.json`, preserving sibling servers. Lifecycle and managed-workstream support are not claimed because Swival's callback contract does not expose a stable session identifier. |
|
||||
| Zero | Supported | `install-mcp --client zero` (native HTTP + bearer in `~/.config/zero/config.json`) + lifecycle hooks via `install-hooks --agent zero --apply` (exec-form native commands in `~/.config/zero/hooks.json`, JSON payload on stdin, no shell). Capture works incl. specialist (subagent) events; no handoff injection — Zero discards `sessionStart` stdout, so recover handoffs via MCP `memory_handoff_list` then `memory_handoff_accept` with that `handoff_id`. |
|
||||
| ZCode | Supported | `install-mcp --client zcode --apply` merges a native HTTP entry (strict schema: `type`/`url`/`headers` only) into the `mcp.servers` map of `~/.zcode/cli/config.json`, preserving sibling servers. ZCode (z.ai, `zcode`, alias `zai`). Lifecycle-hook capture via `install-hooks --agent zcode --apply`: exec-form native commands (`type: "process"`, no shell) merged into the root `hooks` block of `~/.zcode/cli/config.json` around any third-party hooks; native commands enforce capture exclusions. Six documented triggers (`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `Stop`); `PostToolUseFailure` fires instead of `PostToolUse` when a tool throws and lands on the same capture channel with the error preserved. `PermissionRequest` is deliberately not installed — its hook chain races the interactive permission client, so passive capture of that event is unreliable by design. No true session-end — `Stop` is a per-turn boundary, so run `ai-memory finalize-session --agent zcode` after the final turn. Unlike Pool and Zero, `SessionStart` stdout injection works (`hookSpecificOutput.additionalContext`, verified live against the embedded engine v0.16.5), so the prior session's handoff is delivered automatically. No first-party `install-mcp` client and no managed workstream are claimed yet. |
|
||||
|
||||
+5
-3
@@ -38,9 +38,11 @@ $ codex # in the same directory, later
|
||||
|
||||
If an agent has MCP but no lifecycle hook surface, ask it to call
|
||||
`memory_handoff_begin` before quitting. The next hooked agent can still
|
||||
consume that handoff automatically. No-stdout clients (Grok, Zero) should
|
||||
call `memory_handoff_list` on resume, then `memory_handoff_accept` with
|
||||
the listed `handoff_id`; listing does not claim the row.
|
||||
consume that handoff automatically. Grok shows it as `PostToolUse`
|
||||
`additionalContext` after the first tool. Until that tool runs, or if the
|
||||
session never calls one, call `memory_handoff_list` then
|
||||
`memory_handoff_accept` with the listed `handoff_id`. Zero should do that
|
||||
on resume. Listing does not claim the row.
|
||||
|
||||
On a server that distinguishes operators, handoffs belong to their creator by
|
||||
default: the next session for that operator sees their own plus deliberately
|
||||
|
||||
+4
-3
@@ -117,9 +117,10 @@
|
||||
- **"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
|
||||
typed handoff with open questions, next steps, and a session summary. Grok
|
||||
captures lifecycle events but ignores SessionStart stdout, so ask it to call
|
||||
`memory_handoff_accept` when resuming from a handoff. Zero has the same
|
||||
no-stdout behavior and also must call `memory_handoff_accept`.
|
||||
cannot show that text before the first prompt. The first tool's
|
||||
`PostToolUse` hook adds it as `additionalContext`. If the session never
|
||||
calls a tool, ask Grok to call `memory_handoff_accept`. Zero still must
|
||||
call `memory_handoff_accept`.
|
||||
- **"What did we decide about X six weeks ago?"** Use `memory_query X` from
|
||||
the agent for FTS5 fused with entity matches and linked-page expansion (plus
|
||||
vector similarity when an embedder is configured). For a quick terminal-only
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# First PostToolUse of a Grok session: additionalContext is what the
|
||||
# model actually sees. SessionStart and UserPromptSubmit must not fetch.
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "post-tool-use" -Agent "grok"
|
||||
Invoke-AiMemoryHook -Event "post-tool-use" -Agent "grok" -FetchHandoff -GrokPostTool
|
||||
exit 0
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Grok Build CLI post-tool-use hook.
|
||||
# PostToolUse stdout is the channel Grok shows the model
|
||||
# (hookSpecificOutput.additionalContext, after the tool result).
|
||||
# SessionStart and UserPromptSubmit stdout are discarded, so they must
|
||||
# not accept the handoff. This fetch runs once per session.
|
||||
_lib_dir="$(dirname "$0")"
|
||||
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
|
||||
. "$_lib_dir/_lib.sh"
|
||||
@@ -8,8 +12,36 @@ SERVER="${AI_MEMORY_HOOK_URL:-http://127.0.0.1:49374}"
|
||||
PAYLOAD=$(cat)
|
||||
CWD=$(ai_memory_extract_cwd "$PAYLOAD")
|
||||
QS=$(ai_memory_marker_qs "$CWD")
|
||||
SESSION_ID=$(ai_memory_extract_session_id "$PAYLOAD")
|
||||
SESSION_QS=""
|
||||
[ -n "$SESSION_ID" ] && SESSION_QS="&session_id=$(ai_memory_url_encode "$SESSION_ID")"
|
||||
|
||||
printf '%s' "$PAYLOAD" \
|
||||
| ai_memory_post_hook "$SERVER/hook?event=post-tool-use&agent=grok${QS}" >/dev/null 2>&1 || true
|
||||
|
||||
SHOWN_KEY="$SESSION_ID"
|
||||
if [ -z "$SHOWN_KEY" ]; then
|
||||
SHOWN_KEY="grok-post-$(printf '%s' "grok:$CWD" | cksum | awk '{print $1}')"
|
||||
fi
|
||||
SHOWN=$(ai_memory_briefed_file "post-$SHOWN_KEY")
|
||||
if [ -f "$SHOWN" ]; then
|
||||
printf '{}\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$PAYLOAD" in
|
||||
*'"subagentType"'*|*'\"subagentType\"'*|*'"parentSessionId"'*)
|
||||
printf '{}\n'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
BRIEF_QS=$(ai_memory_briefing_qs "$CWD")
|
||||
HANDOFF=$(ai_memory_get_handoff "$SERVER/handoff?agent=grok${QS}${SESSION_QS}${BRIEF_QS}" 2>/dev/null || true)
|
||||
if [ -n "$HANDOFF" ]; then
|
||||
ai_memory_mark_briefed "$SHOWN"
|
||||
CTX=$(printf '%s' "$HANDOFF" | ai_memory_json_string)
|
||||
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":%s}}\n' "$CTX"
|
||||
exit 0
|
||||
fi
|
||||
printf '{}\n'
|
||||
exit 0
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# Grok Build CLI SessionStart hook.
|
||||
# Grok ignores SessionStart stdout, so this hook captures the event only.
|
||||
# Do NOT fetch /handoff here: accepting a handoff is destructive and Grok
|
||||
# would discard the returned context.
|
||||
# would discard the returned context. UserPromptSubmit cannot deliver it
|
||||
# either (allowing-hook stdout is discarded).
|
||||
_lib_dir="$(dirname "$0")"
|
||||
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
|
||||
. "$_lib_dir/_lib.sh"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Do not fetch /handoff. Grok discards allowing UserPromptSubmit stdout,
|
||||
# and GET /handoff would accept the handoff anyway.
|
||||
. "$PSScriptRoot\..\lib\ai-memory-hook.ps1"
|
||||
Invoke-AiMemoryHook -Event "user-prompt" -Agent "grok"
|
||||
exit 0
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Grok Build CLI user-prompt hook.
|
||||
# Do NOT fetch /handoff. Grok discards stdout of an allowing
|
||||
# UserPromptSubmit (no additionalContext), and GET /handoff marks the
|
||||
# handoff accepted. SessionStart must not fetch either. Recover with
|
||||
# MCP memory_handoff_list then memory_handoff_accept.
|
||||
_lib_dir="$(dirname "$0")"
|
||||
[ -f "$_lib_dir/_lib.sh" ] || _lib_dir="$_lib_dir/.."
|
||||
. "$_lib_dir/_lib.sh"
|
||||
|
||||
@@ -308,7 +308,10 @@ function Invoke-AiMemoryHook {
|
||||
# first prompt — parity with Claude's once-per-SessionStart brief).
|
||||
# Later fetches keep the handoff but drop the briefing params so the
|
||||
# server does not recompose the brief per prompt.
|
||||
[switch] $BriefingOncePerSession
|
||||
[switch] $BriefingOncePerSession,
|
||||
# Grok PostToolUse: wrap a fetched handoff as additionalContext and
|
||||
# only fetch once per session. Other events must not set this.
|
||||
[switch] $GrokPostTool
|
||||
)
|
||||
|
||||
$Server = if ($env:AI_MEMORY_HOOK_URL) { $env:AI_MEMORY_HOOK_URL } else { "http://127.0.0.1:49374" }
|
||||
@@ -362,6 +365,7 @@ function Invoke-AiMemoryHook {
|
||||
|
||||
if ($FetchHandoff) {
|
||||
$NativeSessionQS = ""
|
||||
$NativeSessionId = $null
|
||||
try {
|
||||
$ParsedPayload = $Payload | ConvertFrom-Json
|
||||
$NativeSessionId = @(
|
||||
@@ -376,6 +380,16 @@ function Invoke-AiMemoryHook {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
$Shown = $null
|
||||
if ($GrokPostTool) {
|
||||
$ShownKey = [string]$NativeSessionId
|
||||
if (-not $ShownKey) { $ShownKey = "grok-post-$PID" }
|
||||
$Shown = Get-AiMemoryBriefedFile -Key "post-$ShownKey"
|
||||
if (Test-Path $Shown -PathType Leaf) {
|
||||
[Console]::Out.Write("{}")
|
||||
return
|
||||
}
|
||||
}
|
||||
# Once-per-session briefing gate. Marker files are created only for
|
||||
# repositories that opt in. Prefer the native session id when Kimi
|
||||
# supplies one; otherwise use a stable hash of agent+cwd.
|
||||
@@ -396,6 +410,9 @@ function Invoke-AiMemoryHook {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($GrokPostTool -and -not $BriefQS) {
|
||||
$BriefQS = Get-AiMemoryBriefingQuery -Cwd $Cwd
|
||||
}
|
||||
try {
|
||||
$Response = Invoke-WebRequest `
|
||||
-UseBasicParsing `
|
||||
@@ -403,7 +420,15 @@ function Invoke-AiMemoryHook {
|
||||
-Uri "$Server/handoff?agent=$Agent$QS$NativeSessionQS$BriefQS" `
|
||||
-Headers $Headers
|
||||
if ($null -ne $Response -and $Response.Content) {
|
||||
if ($AntigravityPreInvocationOutput) {
|
||||
if ($GrokPostTool) {
|
||||
$Wrapped = @{
|
||||
hookSpecificOutput = @{
|
||||
hookEventName = "PostToolUse"
|
||||
additionalContext = $Response.Content
|
||||
}
|
||||
}
|
||||
[Console]::Out.Write(($Wrapped | ConvertTo-Json -Depth 5 -Compress))
|
||||
} elseif ($AntigravityPreInvocationOutput) {
|
||||
$Payload = @{
|
||||
injectSteps = @(@{ ephemeralMessage = $Response.Content })
|
||||
}
|
||||
@@ -411,14 +436,17 @@ function Invoke-AiMemoryHook {
|
||||
} else {
|
||||
[Console]::Out.Write($Response.Content)
|
||||
}
|
||||
} elseif ($AntigravityPreInvocationOutput) {
|
||||
} elseif ($AntigravityPreInvocationOutput -or $GrokPostTool) {
|
||||
[Console]::Out.Write("{}")
|
||||
}
|
||||
} catch {
|
||||
if ($AntigravityPreInvocationOutput) {
|
||||
if ($AntigravityPreInvocationOutput -or $GrokPostTool) {
|
||||
[Console]::Out.Write("{}")
|
||||
}
|
||||
}
|
||||
if ($Shown) {
|
||||
Set-AiMemoryBriefed -Path $Shown
|
||||
}
|
||||
# Mark the session as briefed only AFTER the GET completed —
|
||||
# success or error (fail-open: with the server down, re-sending the
|
||||
# brief-flagged request on every prompt would deliver nothing
|
||||
|
||||
Reference in New Issue
Block a user