Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/specs/2026-07-30-kimi-host-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,6 @@ failing contract rather than silent misrouting.
- [NEEDS CLARIFICATION: whether Kimi Code will expose custom-agent inventory
natively, at which point the Agents collection could move off the
not-yet-inventoried boundary.]
- [NEEDS CLARIFICATION: Kimi Code does not document a session-id environment
variable; `currentSessionId()` follows the `<HOST>_SESSION_ID` convention and
reads `KIMI_SESSION_ID`, returning null when it is unset.]
8 changes: 7 additions & 1 deletion scripts/agent-customize/core/items.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,13 @@ export async function collectSkillFiles(root, scope, sourceLabel, rootForEvidenc
evidence: evidence(filePath, rootForEvidence),
});
}
return items.sort(sortByName);
// Plugin bundles are third-party content and collection follows symlinks,
// so a symlink inside a plugin component root could otherwise pull files
// from outside it into the inventory; contain plugin collections to the
// component root. User and project scopes intentionally keep
// symlink-installed skills (see the README skill-install recommendation).
const contained = scope === "plugin" ? await filterItemsInsideRoot(items, root) : items;
return contained.sort(sortByName);
}

export async function collectMarkdownItems(root, kind, scope, sourceLabel, rootForEvidence = root) {
Expand Down
12 changes: 11 additions & 1 deletion scripts/session-analysis/platforms/kimi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ function wireEvents(raw, sourceRef, options) {
timestamp: inferTimestamp(raw),
sourceKind: sourceRef.kind,
planningScope: "workspace",
agentId: sourceRef.agentId ?? null,
isSubagent: sourceRef.agentId ? sourceRef.agentId !== "main" : null,
};

Expand Down Expand Up @@ -350,8 +351,11 @@ function finalizeSession(session) {
function dedupeEvents(events) {
const seen = new Set();
return events.filter((event) => {
// Tool call ids are only unique per agent: the main wire and a subagent
// wire of the same session may reuse the same id, so the dedupe key must
// include the agent identity to avoid dropping distinct events.
const key = event.toolInvocationId && event.lifecyclePhase
? `${event.sessionId}:${event.lifecyclePhase}:${event.toolInvocationId}`
? `${event.sessionId}:${event.agentId ?? "main"}:${event.lifecyclePhase}:${event.toolInvocationId}`
: null;
if (!key) return true;
if (seen.has(key)) return false;
Expand All @@ -361,6 +365,12 @@ function dedupeEvents(events) {
}

export class KimiSessionAnalyzer extends SessionAnalyzer {
// Kimi Code does not document a session-id environment variable; follow the
// <HOST>_SESSION_ID convention and return null when it is unset.
currentSessionId() {
return process.env.KIMI_SESSION_ID ?? null;
}

async resolveScope(options = {}) {
const since = normalizeCliDate(options.since, false);
const until = normalizeCliDate(options.until, true);
Expand Down
15 changes: 15 additions & 0 deletions test/session-analysis-fs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,18 @@ test("collectSkillFiles discovers skills installed via symlinks", async () => {
assert.equal(items[0].name, "linked-skill");
});
});

test("collectSkillFiles drops plugin skills whose realpath escapes the component root", async () => {
await withTempDir(async (root) => {
const pluginRoot = path.join(root, "plugin");
const skillsRoot = path.join(pluginRoot, "skills");
await writeSkill(path.join(skillsRoot, "inside-skill"), "inside");
// A symlink inside the plugin component root points at a skill outside it.
const external = path.join(root, "outside-plugin-root", "external-skill");
await writeSkill(external, "external");
await symlink(external, path.join(skillsRoot, "linked-outside"), SYMLINK_TYPE);

const items = await collectSkillFiles(skillsRoot, "plugin", "Plugin", pluginRoot);
assert.deepEqual(items.map((item) => item.name), ["inside-skill"]);
});
});
73 changes: 69 additions & 4 deletions test/session-analysis-providers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,68 @@ test("Kimi keeps partial and malformed usage explicit instead of zero-filling",
assert.equal(Object.hasOwn(usageEvents[0].modelUsage, "cacheReadInputTokens"), false);
});

test("Kimi currentSessionId reads KIMI_SESSION_ID and falls back to null", () => {
const analyzer = new KimiSessionAnalyzer();
const previous = process.env.KIMI_SESSION_ID;
try {
delete process.env.KIMI_SESSION_ID;
assert.equal(analyzer.currentSessionId(), null);
process.env.KIMI_SESSION_ID = "session_fixture-current";
assert.equal(analyzer.currentSessionId(), "session_fixture-current");
} finally {
if (previous === undefined) {
delete process.env.KIMI_SESSION_ID;
} else {
process.env.KIMI_SESSION_ID = previous;
}
}
});

test("Kimi dedupe keeps a shared tool call id per agent but drops repeats within one agent", async () => {
const root = await fixtureRoot("session-kimi-dedupe-agents-");
const home = path.join(root, ".kimi-code");
const workspace = path.join(root, "workspace", "project");
const sessionId = "session_77777777-7777-4777-8777-777777777777";
const sessionDir = path.join(home, "sessions", "wd_project_ab12cd34ef56", sessionId);
await mkdir(workspace, { recursive: true });
await mkdir(home, { recursive: true });
await writeFile(path.join(home, "workspaces.json"), JSON.stringify({
version: 1,
workspaces: { wd_project_ab12cd34ef56: { root: workspace, name: "project" } },
}));
const toolCall = (uuid, time) => ({
type: "context.append_loop_event",
event: {
type: "tool.call",
uuid,
toolCallId: "tool-1",
name: "Bash",
args: { command: "npm test" },
},
time,
});
await writeJsonl(path.join(sessionDir, "agents", "main", "wire.jsonl"), [
{ type: "metadata", protocol_version: "1.4", created_at: Date.parse("2026-07-20T01:00:00.000Z") },
toolCall("main-1", Date.parse("2026-07-20T01:01:00.000Z")),
// A repeated record of the same tool call within one agent wire is a
// duplicate and must still be deduped.
toolCall("main-2", Date.parse("2026-07-20T01:01:01.000Z")),
]);
// A subagent wire reusing the same toolCallId is a distinct event.
await writeJsonl(path.join(sessionDir, "agents", "helper-1", "wire.jsonl"), [
{ type: "metadata", protocol_version: "1.4", created_at: Date.parse("2026-07-20T01:00:00.000Z") },
toolCall("helper-1", Date.parse("2026-07-20T01:02:00.000Z")),
]);

const analyzer = new KimiSessionAnalyzer();
const discovery = await analyzer.analyze({ command: "sources", workspace, home });
const scope = await analyzer.resolveScope({ workspace, home });
const events = await analyzer.readSession(discovery.sessions[0], scope, {});
const calls = events.filter((event) => event.type === "tool.call");
assert.equal(calls.length, 2);
assert.deepEqual(calls.map((event) => event.agentId).sort(), ["helper-1", "main"]);
});

test("Pi provider rejects a transcript whose header cwd belongs to another workspace", async () => {
const root = await fixtureRoot("session-pi-isolation-");
const home = path.join(root, ".pi", "agent");
Expand Down Expand Up @@ -1386,8 +1448,9 @@ test("Kimi provider merges main and subagent wire files and dedupes repeated too
]);
await writeJsonl(path.join(sessionDir, "agents", "researcher", "wire.jsonl"), [
{ type: "metadata", protocol_version: "1.4", created_at: Date.parse("2026-07-20T01:00:00.000Z") },
// Same toolInvocationId + lifecyclePhase as the main wire record: dedupeEvents
// must drop this duplicate even though it comes from another agent's file.
// Same toolInvocationId + lifecyclePhase as the main wire record, but from
// another agent's file: tool call ids are only unique per agent, so
// dedupeEvents must keep this distinct subagent event.
{
type: "context.append_loop_event",
event: { type: "tool.call", uuid: "tool-1", toolCallId: "tool-1", name: "Bash", args: { command: "npm test" } },
Expand Down Expand Up @@ -1417,10 +1480,12 @@ test("Kimi provider merges main and subagent wire files and dedupes repeated too
const scope = await analyzer.resolveScope({ workspace, home });
const events = await analyzer.readSession(discovery.sessions[0], scope, { includeCommandText: true });
const toolCalls = events.filter((event) => event.type === "tool.call");
assert.deepEqual(toolCalls.map((event) => event.toolInvocationId), ["tool-1", "tool-2"]);
// The surviving tool-1 copy is the first occurrence, from the main agent wire file.
// The subagent copy of tool-1 survives: dedupe keys include the agent id.
assert.deepEqual(toolCalls.map((event) => event.toolInvocationId), ["tool-1", "tool-1", "tool-2"]);
assert.deepEqual(toolCalls.map((event) => event.agentId), ["main", "researcher", "researcher"]);
assert.equal(toolCalls[0].isSubagent, false);
assert.equal(toolCalls[1].isSubagent, true);
assert.equal(toolCalls[2].isSubagent, true);
assert.equal(events.filter((event) => event.type === "tool.result").length, 1);
assert.equal(events.filter((event) => event.type === "metadata.wire").length, 2);
});
Expand Down