1jehuang / 1jehuang/jcode

[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

Open
#822 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

autonomous: no enhancement triage: needs-decision
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)

  1. Take pending memory on any turn, not only fresh user turns.
  2. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.