diff --git a/CHANGELOG.md b/CHANGELOG.md index d9182dd3..f65b5f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ номера версий по [семантике](https://semver.org/lang/ru/), то есть до `1.0.0` интерфейс команд ещё может меняться. +## Не выпущено + +### Исправлено + +- **Чат, в который владелец вернулся, больше не остаётся помеченным «покинут»** (cli-core 0.352.0). Неполный + список чатов и вход снимают эту метку с чатов, которые они называют; «в архиве» и другие состояния остаются. + Раньше такой чат мог пропасть из `max chats list`, а `store clear --left` удалял его вместе с сообщениями. + Полный список, как и прежде, помечает «покинут» чаты, которых в нём нет. + ## 0.64.0 — 19.10.2026 ### Что нового diff --git a/docs/dev/architecture/store.md b/docs/dev/architecture/store.md index ad05b0a4..0dca8b7a 100644 --- a/docs/dev/architecture/store.md +++ b/docs/dev/architecture/store.md @@ -34,6 +34,7 @@ with `max messages sync` to make older messages available. ## Departed chats Only a complete nonempty chat snapshot marks missing chats as left. Cut and empty snapshots keep -previous chats. `max store clear --left` previews the data; `--allow-dangerous` removes it. +previous chats. Every snapshot, cut or complete, and every login takes the left mark off the chats it +names; other states such as archived stay. `max store clear --left` previews the data; `--allow-dangerous` removes it. There is no whole-store wipe command. `max doctor` names any remaining legacy profile file; its data is not migrated into the shared store. diff --git a/package.json b/package.json index 1ff67058..6004b7c8 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@msgpack/msgpack": "3.1.3", "@valibot/to-json-schema": "1.8.0", "@wirecat/cli-base": "0.47.0", - "@wirecat/cli-core": "0.350.0", + "@wirecat/cli-core": "0.352.0", "commander": "15.0.0", "lossless-json": "4.3.1", "ogg-opus-decoder": "1.7.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 732dc22a..53534d0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,8 +25,8 @@ importers: specifier: 0.47.0 version: 0.47.0(commander@15.0.0)(typescript@7.0.2)(vitest@5.0.3) '@wirecat/cli-core': - specifier: 0.350.0 - version: 0.350.0(@napi-rs/canvas@1.0.11)(@wirecat/cli-base@0.47.0(commander@15.0.0)(typescript@7.0.2)(vitest@5.0.3))(cac@6.7.14)(commander@15.0.0)(typescript@7.0.2)(zod@4.6.5) + specifier: 0.352.0 + version: 0.352.0(@napi-rs/canvas@1.0.11)(@wirecat/cli-base@0.47.0(commander@15.0.0)(typescript@7.0.2)(vitest@5.0.3))(cac@6.7.14)(commander@15.0.0)(typescript@7.0.2)(zod@4.6.5) commander: specifier: 15.0.0 version: 15.0.0 @@ -1330,8 +1330,8 @@ packages: '@wirecat/cli-core-sqlite@1.0.1': resolution: {integrity: sha512-dFkTE3+XdvZwrt7bZZEd2fzdqR2vFgnnGPOhmBKGNa7uKQHjOdqJL8Q9O5cajIjy5o8Gl0LVn0OZbYajhLvQ1A==} - '@wirecat/cli-core@0.350.0': - resolution: {integrity: sha512-08sMF7YpOBafuDIHw3CwpC9lXymR0UXthptDRJeUeXevm/8JMP8TKS0rwK6IrCJ/ghm9auo1X+4KJ8Ky6L40VQ==} + '@wirecat/cli-core@0.352.0': + resolution: {integrity: sha512-+WJlZASFXINCkgIOpU6qmYHhSlgEpiCGkF23WAb3fcqsi5duu7F1X+26iBJINBi65CrDhtiuolGiaDIwKpRlGQ==} engines: {node: ^22.16.0 || >=24} hasBin: true peerDependencies: @@ -3540,7 +3540,7 @@ snapshots: '@wirecat/cli-core-sqlite@1.0.1': {} - '@wirecat/cli-core@0.350.0(@napi-rs/canvas@1.0.11)(@wirecat/cli-base@0.47.0(commander@15.0.0)(typescript@7.0.2)(vitest@5.0.3))(cac@6.7.14)(commander@15.0.0)(typescript@7.0.2)(zod@4.6.5)': + '@wirecat/cli-core@0.352.0(@napi-rs/canvas@1.0.11)(@wirecat/cli-base@0.47.0(commander@15.0.0)(typescript@7.0.2)(vitest@5.0.3))(cac@6.7.14)(commander@15.0.0)(typescript@7.0.2)(zod@4.6.5)': dependencies: '@anthropic-ai/sdk': 0.131.0(zod@4.6.5) '@bomb.sh/tab': 0.0.22(cac@6.7.14)(commander@15.0.0) diff --git a/src/record.test.ts b/src/record.test.ts index fc6989d9..5d33ac31 100644 --- a/src/record.test.ts +++ b/src/record.test.ts @@ -168,4 +168,75 @@ describe("the record", () => { expect(await store.sync.syncState(key, "login.marker")).toBeUndefined() await store.close() }) + + describe("rejoining chats the owner is back in", () => { + const key = { provider: "max", account: ME } + const other = { provider: "max", account: "10000009" } + const chat = (id: string) => ({ + id, + title: `Chat ${id}`, + kind: "group" as const, + unreadCount: 0, + lastMessageAt: null, + participantsCount: null, + }) + const seeded = async () => { + const { env } = setUp() + const store = await openStore({ env }) + await store.chats.saveChats(key, [ + { ...chat("111"), membershipState: "left" }, + { ...chat("222"), membershipState: "archived" }, + chat("333"), + ]) + await store.chats.saveChats(other, [{ ...chat("111"), membershipState: "left" }]) + await store.close() + return { env, record: maxRecord({ account: () => ME, env }) } + } + const states = async (env: { MESSAGING_STORE: string }) => { + const store = await openStore({ env }) + const left = await store.chats.leftChats(key) + const all = (await store.chats.chats(key, {})).items + const otherLeft = await store.chats.leftChats(other) + await store.close() + return { + left: left.chats, + otherLeft: otherLeft.chats, + listed: all.map((stored) => [stored.id, stored.membershipState]), + } + } + + it("a cut-short list takes the left mark off a chat it names and keeps the archived one", async () => { + const { env, record } = await seeded() + const known = await record.rememberChats([chat("111"), chat("222")], false) + await record.close() + + expect(known.map((stored) => stored.id).sort()).toEqual(["111", "222", "333"]) + expect(await states(env)).toMatchObject({ + left: 0, + otherLeft: 1, + listed: expect.arrayContaining([["222", "archived"]]), + }) + }) + + it("a complete list still marks the chats it leaves out as left", async () => { + const { env, record } = await seeded() + const known = await record.rememberChats([chat("111"), chat("222")], true) + await record.close() + + expect(known.map((stored) => stored.id).sort()).toEqual(["111", "222"]) + expect(await states(env)).toMatchObject({ left: 1, listed: expect.arrayContaining([["222", "archived"]]) }) + }) + + it("a login takes the left mark off the chats it names and keeps the archived one", async () => { + const { env, record } = await seeded() + await record.applyLogin({ chats: [chat("111"), chat("222")], people: [], members: new Map() }) + await record.close() + + expect(await states(env)).toMatchObject({ + left: 0, + otherLeft: 1, + listed: expect.arrayContaining([["222", "archived"]]), + }) + }) + }) }) diff --git a/src/record.ts b/src/record.ts index 5723055c..3b5fc58e 100644 --- a/src/record.ts +++ b/src/record.ts @@ -79,6 +79,11 @@ export const maxRecord = ({ account, env }: { account: () => Id | undefined; env members, ...(marker === undefined ? {} : { state: { [MARKER]: String(marker) } }), }) + if (chats.length > 0) + await db.chats.rejoinChats( + key, + chats.map((chat) => chat.id), + ) return { known: (await db.people.people("max", { account: key.account })).all().length, added, @@ -103,11 +108,9 @@ export const maxRecord = ({ account, env }: { account: () => Id | undefined; env if (!key) return chats const db = await store() await db.chats.saveChats(key, chats) - if (complete && chats.length > 0) - await db.chats.markChatsLeft( - key, - chats.map((chat) => chat.id), - ) + const ids = chats.map((chat) => chat.id) + if (complete && ids.length > 0) await db.chats.markChatsLeft(key, ids) + else if (ids.length > 0) await db.chats.rejoinChats(key, ids) return (await db.chats.chats(key, {})).items },