fix(workstream): accept current Kimi session cwd (#382)

This commit is contained in:
AkitaOnRails
2026-08-07 14:51:06 -03:00
parent d9b90121c4
commit ad7243eebe
4 changed files with 97 additions and 18 deletions
+5
View File
@@ -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
+81 -11
View File
@@ -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();
+8 -5
View File
@@ -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
+3 -2
View File
@@ -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