mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
fix(workstream): accept current Kimi session cwd (#382)
This commit is contained in:
@@ -42,6 +42,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
query ([#374]).
|
||||
|
||||
### Fixed
|
||||
- Kimi Code 0.34.0 managed runs now discover checkout-local sessions from the
|
||||
current `state.json` `cwd` field as well as the legacy `workDir` alias. The
|
||||
parser rejects conflicting aliases and persisted ids that disagree with the
|
||||
session directory, and deterministic acceptance now exercises the current
|
||||
state schema (#382).
|
||||
- Prevented delayed post-tool and shutdown hook tails from redirecting the
|
||||
shared active-project fallback after work moved to another project. Only
|
||||
session starts, user prompts, and pre-tool events now advance shared or
|
||||
|
||||
@@ -2166,9 +2166,11 @@ fn session_header_for_cwd(
|
||||
/// Kimi sessions are self-describing in `<session-dir>/state.json` — the wire
|
||||
/// journal itself carries no session id or cwd, and the bucket directory name
|
||||
/// is a one-way hash of the cwd, so neither can be inferred from the layout.
|
||||
/// The journal path is `<session-dir>/agents/main/wire.jsonl`, making the
|
||||
/// session directory the third ancestor. Missing/invalid state means the
|
||||
/// session is unusable for checkout matching, not an error.
|
||||
/// Kimi 0.29 used `workDir`; 0.34 uses `cwd`. Conflicting aliases and an
|
||||
/// optional id that disagrees with the directory fail closed. The journal path
|
||||
/// is `<session-dir>/agents/main/wire.jsonl`, making the session directory the
|
||||
/// third ancestor. Missing/invalid state means the session is unusable for
|
||||
/// checkout matching, not an error.
|
||||
fn kimi_session_header(path: &Path) -> Result<Option<(String, PathBuf)>> {
|
||||
let Some(session_dir) = path.ancestors().nth(3) else {
|
||||
return Ok(None);
|
||||
@@ -2179,12 +2181,33 @@ fn kimi_session_header(path: &Path) -> Result<Option<(String, PathBuf)>> {
|
||||
let Ok(state) = serde_json::from_str::<Value>(&raw) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(cwd) = state.get("workDir").and_then(Value::as_str) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(id) = session_dir.file_name().and_then(|name| name.to_str()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
match state.get("id") {
|
||||
Some(Value::String(recorded)) if recorded == id => {}
|
||||
Some(_) => return Ok(None),
|
||||
None => {}
|
||||
}
|
||||
let legacy = match state.get("workDir") {
|
||||
Some(Value::String(cwd)) => Some(cwd.as_str()),
|
||||
Some(_) => return Ok(None),
|
||||
None => None,
|
||||
};
|
||||
let current = match state.get("cwd") {
|
||||
Some(Value::String(cwd)) => Some(cwd.as_str()),
|
||||
Some(_) => return Ok(None),
|
||||
None => None,
|
||||
};
|
||||
let cwd = match (legacy, current) {
|
||||
(Some(legacy), Some(current))
|
||||
if legacy == current || same_path(Path::new(legacy), Path::new(current)) =>
|
||||
{
|
||||
current
|
||||
}
|
||||
(Some(_), Some(_)) | (None, None) => return Ok(None),
|
||||
(Some(cwd), None) | (None, Some(cwd)) => cwd,
|
||||
};
|
||||
Ok(Some((id.to_string(), PathBuf::from(cwd))))
|
||||
}
|
||||
|
||||
@@ -3278,15 +3301,18 @@ mod tests {
|
||||
/// `session_b` at `other`. Returns `(root, wire_a)`.
|
||||
fn kimi_store_fixture(cwd: &Path, other: &Path) -> (tempfile::TempDir, PathBuf) {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
for (bucket, id, work_dir) in [
|
||||
("wd_repo_a1b2c3d4e5f6", "session_aaa", cwd),
|
||||
("wd_other_f6e5d4c3b2a1", "session_bbb", other),
|
||||
for (bucket, id, locator_key, work_dir) in [
|
||||
("wd_repo_a1b2c3d4e5f6", "session_aaa", "workDir", cwd),
|
||||
("wd_other_f6e5d4c3b2a1", "session_bbb", "cwd", other),
|
||||
] {
|
||||
let session_dir = root.path().join(bucket).join(id);
|
||||
fs::create_dir_all(session_dir.join("agents/main")).unwrap();
|
||||
let mut state = serde_json::Map::new();
|
||||
state.insert("id".into(), Value::String(id.into()));
|
||||
state.insert(locator_key.into(), json!(work_dir));
|
||||
fs::write(
|
||||
session_dir.join("state.json"),
|
||||
json!({"workDir": work_dir}).to_string(),
|
||||
Value::Object(state).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -3314,7 +3340,7 @@ mod tests {
|
||||
.join("wd_other_f6e5d4c3b2a1/session_bbb/agents/main/wire.jsonl");
|
||||
fs::write(&wire_b, "").unwrap();
|
||||
// The "other" bucket is alphabetically first and its session newer,
|
||||
// so only an exact workDir match can pick the right session.
|
||||
// so only an exact state locator match can pick the right session.
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
fs::write(&wire_b, "{\"type\":\"metadata\"}\n").unwrap();
|
||||
|
||||
@@ -3382,6 +3408,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_state_rejects_conflicting_locators_and_mismatched_ids() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let cwd = temp.path().join("repo");
|
||||
let other = temp.path().join("other");
|
||||
let session_dir = temp.path().join("session_expected");
|
||||
let wire = session_dir.join("agents/main/wire.jsonl");
|
||||
fs::create_dir_all(wire.parent().unwrap()).unwrap();
|
||||
fs::create_dir_all(&cwd).unwrap();
|
||||
fs::create_dir_all(&other).unwrap();
|
||||
fs::write(&wire, "").unwrap();
|
||||
|
||||
fs::write(
|
||||
session_dir.join("state.json"),
|
||||
json!({"id":"session_expected","workDir":cwd,"cwd":other}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(kimi_session_header(&wire).unwrap().is_none());
|
||||
|
||||
fs::write(
|
||||
session_dir.join("state.json"),
|
||||
json!({"id":"session_other","cwd":cwd}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(kimi_session_header(&wire).unwrap().is_none());
|
||||
|
||||
fs::write(
|
||||
session_dir.join("state.json"),
|
||||
json!({"id":"session_expected","cwd":[cwd]}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(kimi_session_header(&wire).unwrap().is_none());
|
||||
|
||||
fs::write(
|
||||
session_dir.join("state.json"),
|
||||
json!({"id":"session_expected","workDir":cwd,"cwd":cwd}).to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
kimi_session_header(&wire).unwrap(),
|
||||
Some(("session_expected".into(), cwd))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_export_maps_visible_records_and_excludes_private_ones() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -310,7 +310,9 @@ Known Kimi Code adapter limitations: subagent transcripts
|
||||
(`agents/<id>/wire.jsonl` other than `main`) are not imported in v1 and are
|
||||
recorded as an extraction-loss annotation; the session bucket directory name
|
||||
is a one-way hash of the working directory, so discovery always reads
|
||||
`state.json`'s `workDir` and never parses the bucket name. Event ids derive
|
||||
`state.json`'s current `cwd` field or legacy `workDir` alias and never parses
|
||||
the bucket name. Conflicting aliases or a persisted id that disagrees with the
|
||||
session directory are rejected. Event ids derive
|
||||
from the SHA-256 of the raw wire.jsonl line, so two byte-identical lines —
|
||||
only possible with identical content in the same millisecond, because Kimi
|
||||
Code stamps each record with `time` — collapse into a single ledger event.
|
||||
@@ -322,7 +324,7 @@ workstream.
|
||||
Legacy sessions that keep `wire.jsonl` directly in the session directory
|
||||
(the pre-`agents/` layout the kimi session-store still reads through its
|
||||
stat fallback) are neither discovered nor imported in v1. The native
|
||||
contract was verified against Kimi Code v0.29.0. The managed launcher accepts
|
||||
contract was reverified against Kimi Code v0.34.0. The managed launcher accepts
|
||||
`kimi`, `kimi-code`, and `kimi-cli`; all three resolve the installed `kimi`
|
||||
executable.
|
||||
|
||||
@@ -468,9 +470,10 @@ 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`), Kiro v2 (`<uuid>.json`'s `cwd`), Kiro
|
||||
v3 (`session.json`'s `workspacePaths`), OMP, and Antigravity may still reference
|
||||
the old path; Crush's project-local `.crush` database moves with the checkout.
|
||||
Pi, Kimi Code (`state.json`'s `cwd` or legacy `workDir`), Kiro v2
|
||||
(`<uuid>.json`'s `cwd`), Kiro v3 (`session.json`'s `workspacePaths`), 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
|
||||
|
||||
@@ -306,12 +306,13 @@ case "${AI_MEMORY_ACCEPTANCE_FAKE_MODE:-argv}" in
|
||||
fi
|
||||
# The bucket directory name is intentionally opaque: the real layout
|
||||
# hashes the working directory one-way, and discovery must read
|
||||
# state.json's workDir instead of parsing the bucket name.
|
||||
# state.json's current cwd field instead of parsing the bucket name.
|
||||
session_dir="${KIMI_CODE_HOME:?kimi fake mode requires KIMI_CODE_HOME}/sessions/wd_fixture_bucket/$session_id"
|
||||
mkdir -p "$session_dir/agents/main"
|
||||
wire="$session_dir/agents/main/wire.jsonl"
|
||||
if [ ! -f "$session_dir/state.json" ]; then
|
||||
printf '{"workDir":"%s"}\n' "$PWD" >"$session_dir/state.json"
|
||||
printf '{"id":"%s","version":2,"cwd":"%s"}\n' \
|
||||
"$session_id" "$PWD" >"$session_dir/state.json"
|
||||
printf '{"type":"metadata","protocol_version":"1","created_at":%s}\n' \
|
||||
"$(date +%s)000" >"$wire"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user