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
5 changes: 5 additions & 0 deletions .changeset/session-resume-updated-at.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix a resumed session being marked as just updated and jumping to the top of the session list without any new activity.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
* and persisted on load for documents written before the seeding existed
* (without touching `updatedAt`, so a format heal never reorders session
* listings) — keeping sessions on a shared `KIMI_CODE_HOME` resumable by
* released v1 builds. Bound at Session scope.
* released v1 builds. Re-registering an agent whose metadata is unchanged is
* a no-op (no write, no mirror, no event), so resuming a session — which
* re-registers its agents as they materialize — never bumps `updatedAt` and
* never reorders session listings. Bound at Session scope.
*
* Read-model mirroring (flag `persistence_minidb_readmodel`): after a metadata
* update is persisted, the fresh summary is mirrored into the `IQueryStore`
Expand Down Expand Up @@ -99,6 +102,8 @@ export class SessionMetadata extends Disposable implements ISessionMetadata {
async registerAgent(agentId: string, meta: AgentMeta): Promise<void> {
return this.enqueueUpdate(async () => {
await this.ready;
const existing = this.data.agents?.[agentId];
if (existing !== undefined && agentMetaEquals(existing, meta)) return;
const agents = { ...this.data.agents, [agentId]: meta };
await this.applyUpdate({ agents });
});
Expand Down Expand Up @@ -162,6 +167,25 @@ export class SessionMetadata extends Disposable implements ISessionMetadata {
}
}

function agentMetaEquals(a: AgentMeta, b: AgentMeta): boolean {
return (
a.homedir === b.homedir &&
a.type === b.type &&
(a.parentAgentId ?? null) === (b.parentAgentId ?? null) &&
a.forkedFrom === b.forkedFrom &&
a.swarmItem === b.swarmItem &&
recordEquals(a.labels, b.labels)
);
}

function recordEquals(a: AgentMeta['labels'], b: AgentMeta['labels']): boolean {
const entriesA = Object.entries(a ?? {});
const entriesB = Object.entries(b ?? {});
return (
entriesA.length === entriesB.length && entriesA.every(([key, value]) => b?.[key] === value)
);
}

export function normalizeSessionMeta(raw: SessionMeta, sessionId: string): SessionMeta {
const legacy = raw as unknown as {
createdAt?: unknown;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,88 @@ describe('SessionMetadata', () => {
'agent-1',
]);
});

it('treats re-registering an unchanged agent as a no-op', async () => {
const meta = ix.get(ISessionMetadata);
await meta.registerAgent('main', {
homedir: '/tmp/sessions/wd_test/s1/agents/main',
type: 'main',
parentAgentId: undefined,
forkedFrom: undefined,
labels: undefined,
});

const before = (await meta.read()).updatedAt;
await new Promise((r) => setTimeout(r, 2));

// A resumed session re-registers its materialized agents; with identical
// metadata that must not write, bump updatedAt, or fire an event.
let fired = 0;
const sub = meta.onDidChangeMetadata(() => {
fired++;
});
await meta.registerAgent('main', {
homedir: '/tmp/sessions/wd_test/s1/agents/main',
type: 'main',
parentAgentId: undefined,
forkedFrom: undefined,
labels: undefined,
});

expect(fired).toBe(0);
expect((await meta.read()).updatedAt).toBe(before);
sub.dispose();
});

it('stays a no-op when re-registering against a persisted document', async () => {
// The document as it lands on disk: keys with undefined values are gone,
// and a legacy writer stored parentAgentId: null. A server restart then
// re-registers `main` with explicit undefineds — still no update.
const store = ix.get(IAtomicDocumentStore);
await store.set(META_SCOPE, 'state.json', {
id: 's1',
version: 2,
createdAt: 1700000000000,
updatedAt: 1700000000000,
archived: false,
agents: {
main: {
homedir: '/tmp/sessions/wd_test/s1/agents/main',
type: 'main',
parentAgentId: null,
},
},
});

const meta = ix.get(ISessionMetadata);
await meta.registerAgent('main', {
homedir: '/tmp/sessions/wd_test/s1/agents/main',
type: 'main',
parentAgentId: undefined,
forkedFrom: undefined,
labels: undefined,
});

expect((await meta.read()).updatedAt).toBe(1700000000000);
});

it('updates when re-registering with changed fields', async () => {
const meta = ix.get(ISessionMetadata);
await meta.registerAgent('main', {
homedir: '/tmp/sessions/wd_test/s1/agents/main',
type: 'main',
});
const before = (await meta.read()).updatedAt;
await new Promise((r) => setTimeout(r, 2));

await meta.registerAgent('main', {
homedir: '/tmp/sessions/wd_test/s1/agents/main',
type: 'main',
labels: { swarmItem: 'src/a.ts' },
});

const next = await meta.read();
expect(next.agents?.['main']?.labels).toEqual({ swarmItem: 'src/a.ts' });
expect(next.updatedAt).toBeGreaterThan(before);
});
});
Loading