[Enhancement]: Memory retrieval only runs on fresh user turns — ACP tasks never get relevant memories because the real question arrives mid-turn via a tool call
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 19.9k
- Forks
- 2.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 30
Description
Summary
The memory pipeline (build_memory_prompt_nonblocking_shared in
crates/jcode-app-core/src/agent/prompting.rs) feeds the memory agent's context
only on fresh user turns and only takes pending memory on those turns. In ACP
host mode (e.g. Multica, hermes-family), a task is a single user prompt followed
by a long tool loop, and the actual question does not live in that prompt — it
arrives mid-turn, from a tool such as multica issue get <id>. Because no second
fresh user turn ever comes until the task ends, memory retrieval only ever sees
the task-instruction boilerplate, so relevant facts are never injected.
Impact
Any ACP host that dispatches tasks as "one prompt + tool loop" (task tickets,
issues, review requests) gets near-zero useful memory injection. The first user
message is usually role boilerplate ("You are running as a local coding agent for
a Multica workspace. Start by running multica issue get <id>...") — embedding
that text cannot surface knowledge about the actual task (e.g. which token env var
the task asks about). The agent then answers from the transcript alone, and the
extraction side under-learns because it also only processes that boilerplate
window.
Root cause
crates/jcode-app-core/src/agent/prompting.rs:
let fresh_user_turn = crate::message::ends_with_fresh_user_turn(&messages);
let pending = if fresh_user_turn {
crate::memory::take_pending_memory(session_id)
} else {
None
};
// Relevance results are consumed only at the start of a fresh user turn.
// Enqueuing again after every tool result runs the local embedding model
// for each provider continuation without creating an additional injection
// opportunity. One update per user turn keeps memory current while avoiding
// redundant 512-token inference during tool-heavy agent loops.
if fresh_user_turn {
crate::memory_agent::update_context_sync_with_dir(
session_id,
messages,
self.session.working_dir.clone(),
);
}
The "one update per user turn" rationale is correct for interactive CLIs, where
each user message carries fresh intent. It breaks for ACP-style tasks where the
intent is revealed by tool output after the initial prompt.
Proposed change (implemented and verified locally)
- Take pending memory on any turn, not only fresh user turns.
- Re-feed the memory agent's context on tool continuations too, throttled to
at most once per ~20s per session so a tool-heavy loop does not re-embed on
every call:
let pending = crate::memory::take_pending_memory(session_id);
let now = std::time::Instant::now();
let last_update = self.session_state_memory_last_update.lock().unwrap();
let should_update = last_update
.map(|t| now.duration_since(t).as_secs() >= 20)
.unwrap_or(true);
if should_update {
crate::memory_agent::update_context_sync_with_dir(
session_id,
messages,
self.session.working_dir.clone(),
);
*self.session_state_memory_last_update.lock().unwrap() = Some(now);
}
The throttle keeps local embedding cost bounded (one 512-token inference per
session per ~20s at most) while making sure the retrieval context advances past
the initial boilerplate once the task question is in the transcript.
Environment
- jcode v0.68.0, Linux x86_64, DeepSeek provider, local all-MiniLM-L6-v2 embeddings.
- ACP protocolVersion 1, host: Multica daemon (hermes-family backend).
- Verified end-to-end: after this change the judge re-ran on the tool continuation
and injected the correct token fact; before it, no relevant fact was ever retrieved.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in crates/jcode-app-core/src/agent/prompting.rs at build_memory_prompt_nonblocking_shared, then read ends_with_fresh_user_turn and the memory-agent update path. Verify how session state can track the last update and how pending memory is consumed during tool continuations. Done means ACP tasks advance retrieval beyond the initial prompt without re-embedding every tool result, and the reported end-to-end judge scenario still retrieves the relevant fact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- ai, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100