@wirecat/cli-core 0.544.0 → 0.545.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cli/messenger/archive-commands.js +20 -2
- package/dist/cli/messenger/backfill-command.js +13 -2
- package/dist/cli/messenger/context.js +3 -0
- package/dist/cli/messenger/history-gaps.d.ts +12 -0
- package/dist/cli/messenger/history-gaps.js +41 -0
- package/dist/cli/messenger/messages-command.js +7 -3
- package/dist/cli/messenger/messages-search-command.js +5 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -2
- package/dist/cli/messenger/search-answers.js +12 -5
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/stored.d.ts +6 -1
- package/dist/cli/messenger/stored.js +38 -2
- package/dist/cli/settings.d.ts +9 -0
- package/dist/cli/settings.js +20 -0
- package/dist/cli/shared-command-exports.d.ts +24 -0
- package/dist/cli/steps.js +1 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/message-transfer.d.ts +4 -0
- package/dist/entities/message-transfer.js +2 -0
- package/dist/services/history-limits.d.ts +45 -0
- package/dist/services/history-limits.js +50 -0
- package/dist/store/chats.d.ts +10 -0
- package/dist/store/chats.js +25 -0
- package/dist/store/sqlite/chats.js +2 -1
- package/dist/store/sqlite/history-skips.d.ts +24 -0
- package/dist/store/sqlite/history-skips.js +95 -0
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.545.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
15
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
16
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
17
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
18
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
19
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
20
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
21
|
+
|
|
10
22
|
## 0.544.0 — 02.11.2026
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -12,11 +12,12 @@ import { kindsOf } from "../../services/inbox.js";
|
|
|
12
12
|
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
14
14
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
15
|
-
import { commandAt } from "../steps.js";
|
|
15
|
+
import { commandAt, lineOf } from "../steps.js";
|
|
16
16
|
import { jobsCommand } from "./backfill-command.js";
|
|
17
17
|
import { messengerContext } from "./context.js";
|
|
18
18
|
import { admitPassword, linesOf, openFolder, runFileFor, saveManifest, sealRun, writeChanges } from "./export-folder.js";
|
|
19
19
|
import { gapsCommand } from "./gaps-command.js";
|
|
20
|
+
import { historyLimitGaps } from "./history-gaps.js";
|
|
20
21
|
import { messageItems } from "./message-items.js";
|
|
21
22
|
import { ENCRYPT_OPTION, passwordOf } from "./password.js";
|
|
22
23
|
import { storeMaintenanceCommands } from "./store-maintenance-command.js";
|
|
@@ -71,9 +72,26 @@ const statusCommand = (messenger) => annotate(new Command("status"), { output: {
|
|
|
71
72
|
.action(async function (chat) {
|
|
72
73
|
const context = messengerContext(this, messenger);
|
|
73
74
|
const rows = await context.withServices((services) => services.archive.status(chat));
|
|
74
|
-
|
|
75
|
+
const skips = await context
|
|
76
|
+
.withStore((store, account) => store.chats.historySkips(account))
|
|
77
|
+
.then((all) => chat === undefined ? all : all.filter((skip) => rows.some((row) => row.chatId === skip.chatId)))
|
|
78
|
+
.catch(() => []);
|
|
79
|
+
const limited = historyLimitGaps(skips);
|
|
80
|
+
renderAnswer(this, context, listOf("store status", {
|
|
81
|
+
items: rows.map(snakeKeys),
|
|
82
|
+
limit: Math.max(1, rows.length),
|
|
83
|
+
has_more: false,
|
|
84
|
+
next: limited.next,
|
|
85
|
+
missing: limited.missing,
|
|
86
|
+
}));
|
|
75
87
|
if (rows.length === 0 && context.format === "pretty")
|
|
76
88
|
context.renderer.note("the store holds no messages for this profile yet");
|
|
89
|
+
if (skips.length > 0 && context.format === "pretty")
|
|
90
|
+
context.renderer.note(`history limits left out pushed history of ${skips.length} chats — \`${lineOf(this, "messages sync <chat> --full-history")}\` fetches one and keeps it in full:\n` +
|
|
91
|
+
skips
|
|
92
|
+
.slice(0, 20)
|
|
93
|
+
.map((skip) => ` ${skip.title ?? skip.chatId} ${skip.messages} messages ${skip.reason}`)
|
|
94
|
+
.join("\n"));
|
|
77
95
|
});
|
|
78
96
|
const LISTED = 20;
|
|
79
97
|
/** Titles are the owner's: these lines go to a person's terminal, never into an error message. */
|
|
@@ -58,14 +58,17 @@ export const syncCommand = (messenger) => {
|
|
|
58
58
|
.option("--catch-up-chunks <n>", "at most this many local vector chunks", wholeNumber)
|
|
59
59
|
.option("--catch-up-messages <n>", "skip a graph rebuild larger than this many messages", wholeNumber)
|
|
60
60
|
.option("--catch-up-time <duration>", "local preparation time budget, 30s by default")
|
|
61
|
+
.option("--full-history", "keep this chat's pushed history in full from now on, past the history limits (history_* settings)")
|
|
61
62
|
.option("--background", "run as a job that outlives this command; `store jobs show` follows it")
|
|
62
63
|
.option("--dry-run", "only estimate how many messages, requests and minutes a full fetch would still take — from the store, no request")
|
|
63
64
|
.action(async function (chat) {
|
|
64
|
-
const { all, pause, sinceTime: since, last, background, dryRun: estimate, catchUp, catchUpChunks, catchUpMessages, catchUpTime, ...sizes } = this.opts();
|
|
65
|
+
const { all, pause, sinceTime: since, last, background, dryRun: estimate, catchUp, catchUpChunks, catchUpMessages, catchUpTime, fullHistory: include, ...sizes } = this.opts();
|
|
65
66
|
if ((chat === undefined) === (all !== true))
|
|
66
67
|
throw new CliError("validation_error", "name a chat, or --all for every chat");
|
|
67
68
|
if (all && estimate)
|
|
68
69
|
throw new CliError("validation_error", "--dry-run prices one chat; not with --all");
|
|
70
|
+
if (include && chat === undefined)
|
|
71
|
+
throw new CliError("validation_error", "--full-history keeps one chat: name it");
|
|
69
72
|
if (all && messenger.history === "store") {
|
|
70
73
|
throw new CliError("validation_error", `--all walks the messenger's chat list, and ${messenger.name ?? messenger.app.command} pushes its chats ` +
|
|
71
74
|
`instead — keep \`${lineOf(this, "serve")}\` running${messenger.historyOnDemand ? ", or name one chat" : ""}`);
|
|
@@ -164,6 +167,10 @@ export const syncCommand = (messenger) => {
|
|
|
164
167
|
const result = await context.withServices((services) => chat === undefined ? services.archive.fetchAll(options) : services.archive.fetch(chat, options));
|
|
165
168
|
if (jobId)
|
|
166
169
|
finishJob(messenger.app, jobs, jobId, { finishedAt: new Date().toISOString(), result });
|
|
170
|
+
const synced = "chat" in result ? result.chat : null;
|
|
171
|
+
const included = include && synced
|
|
172
|
+
? await context.withStore((store, account) => store.chats.setHistoryIncluded(account, synced, true))
|
|
173
|
+
: undefined;
|
|
167
174
|
const tasks = await taskRulesAfterSync(context);
|
|
168
175
|
const prepared = "prepared" in result ? result.prepared : undefined;
|
|
169
176
|
const unprepared = prepared && !prepared.complete && "chat" in result && result.chat !== null;
|
|
@@ -171,7 +178,11 @@ export const syncCommand = (messenger) => {
|
|
|
171
178
|
const review = await context.withServices((services) => services.mentions.newCount(point.after));
|
|
172
179
|
const toReview = review.count;
|
|
173
180
|
const shown = `${toReview}${review.more ? "+" : ""}`;
|
|
174
|
-
renderAnswer(this, context, itemOf(snakeKeys(
|
|
181
|
+
renderAnswer(this, context, itemOf(snakeKeys({
|
|
182
|
+
...result,
|
|
183
|
+
...(tasks && tasks.added + tasks.closed + (tasks.held ?? 0) > 0 ? { tasks } : {}),
|
|
184
|
+
...(included === undefined ? {} : { historyIncluded: included }),
|
|
185
|
+
}), {
|
|
175
186
|
next: [
|
|
176
187
|
...(unprepared
|
|
177
188
|
? [
|
|
@@ -231,6 +231,9 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
231
231
|
recipientIds: () => (new RecipientList(recipientsPathFor(app, settings.profile, env), app.command).read() ?? []).flatMap((recipient) => [recipient.id, ...(recipient.partnerId ? [recipient.partnerId] : [])]),
|
|
232
232
|
...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
|
|
233
233
|
...(unreadCounts === "store" ? { countsUnread: true } : {}),
|
|
234
|
+
...(history === "store" && settings.historyLimits
|
|
235
|
+
? { historyLimits: settings.historyLimits, command: `${app.command} ${settings.profile}` }
|
|
236
|
+
: {}),
|
|
234
237
|
});
|
|
235
238
|
const passives = history === "store" && self !== null && !watching
|
|
236
239
|
? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn, closeWaitMs))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Missing, NextStep } from "@wirecat/cli-base/standards";
|
|
2
|
+
import type { AccountKey, MessageStore, StoredHistorySkip } from "../../store/store.js";
|
|
3
|
+
/**
|
|
4
|
+
* What the history limits (`history_*` settings) left out of pushed history, as `missing` and `next`: an answer read
|
|
5
|
+
* from the store must not pass for every message the messenger has.
|
|
6
|
+
*/
|
|
7
|
+
export declare const historyLimitGaps: (skips: StoredHistorySkip[]) => {
|
|
8
|
+
next: NextStep[];
|
|
9
|
+
missing: Missing[];
|
|
10
|
+
};
|
|
11
|
+
/** What the limits left out in every account an answer read, most messages first. */
|
|
12
|
+
export declare const historySkipsOf: (store: Pick<MessageStore, "chats">, accounts: readonly AccountKey[]) => Promise<StoredHistorySkip[]>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nextStep } from "../steps.js";
|
|
2
|
+
const SHOWN = 5;
|
|
3
|
+
const reasonOf = (skip) => skip.reason === "group_too_large"
|
|
4
|
+
? `a group of ${skip.members ?? "many"} people, not saved`
|
|
5
|
+
: `only from ${skip.keptFrom?.slice(0, 10) ?? "the window's start"}`;
|
|
6
|
+
const nameOf = (skip) => skip.title ?? skip.chatId;
|
|
7
|
+
/**
|
|
8
|
+
* What the history limits (`history_*` settings) left out of pushed history, as `missing` and `next`: an answer read
|
|
9
|
+
* from the store must not pass for every message the messenger has.
|
|
10
|
+
*/
|
|
11
|
+
export const historyLimitGaps = (skips) => {
|
|
12
|
+
const [first] = skips;
|
|
13
|
+
if (!first)
|
|
14
|
+
return { next: [], missing: [] };
|
|
15
|
+
const messages = skips.reduce((sum, skip) => sum + skip.messages, 0);
|
|
16
|
+
const listed = skips
|
|
17
|
+
.slice(0, SHOWN)
|
|
18
|
+
.map((skip) => `${nameOf(skip)} (${skip.messages}, ${reasonOf(skip)})`)
|
|
19
|
+
.join("; ");
|
|
20
|
+
const more = skips.length > SHOWN ? `; and ${skips.length - SHOWN} more — \`store status\` lists them` : "";
|
|
21
|
+
return {
|
|
22
|
+
next: [
|
|
23
|
+
{
|
|
24
|
+
...nextStep("messages.sync", { args: { chat: first.chatId, full_history: true } }),
|
|
25
|
+
why: "fetches this chat's history and keeps it in full from now on, past the history limits",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
missing: [
|
|
29
|
+
{
|
|
30
|
+
field: "history_limits",
|
|
31
|
+
why: `the history limits (history_* settings) left ${messages} pushed messages of ${skips.length} chats out of ` +
|
|
32
|
+
`the store: ${listed}${more}`,
|
|
33
|
+
step: "messages.sync",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** What the limits left out in every account an answer read, most messages first. */
|
|
39
|
+
export const historySkipsOf = async (store, accounts) => (await Promise.all(accounts.map((account) => store.chats.historySkips(account))))
|
|
40
|
+
.flat()
|
|
41
|
+
.sort((a, b) => b.messages - a.messages);
|
|
@@ -18,6 +18,7 @@ import { messengerContext } from "./context.js";
|
|
|
18
18
|
import { linksCommand } from "./conversations-command.js";
|
|
19
19
|
import { downloadSubcommand } from "./download-command.js";
|
|
20
20
|
import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
|
|
21
|
+
import { historyLimitGaps } from "./history-gaps.js";
|
|
21
22
|
import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
|
|
22
23
|
import { hideNever } from "./lacks.js";
|
|
23
24
|
import { threadAnswer } from "./message-answers.js";
|
|
@@ -74,9 +75,12 @@ export const messagesCommand = (messenger) => {
|
|
|
74
75
|
: undefined;
|
|
75
76
|
// Offline, the store is the whole answer: say how much of the chat it holds. Online the messenger is read.
|
|
76
77
|
const gaps = context.settings.offline
|
|
77
|
-
?
|
|
78
|
-
await storedChatId(messenger, chat, store, account, { includeLeft: true })
|
|
79
|
-
|
|
78
|
+
? await context.withStore(async (store, account) => {
|
|
79
|
+
const chatId = await storedChatId(messenger, chat, store, account, { includeLeft: true });
|
|
80
|
+
const held = coverageGaps(await chatCoverage(store, account, [chatId]));
|
|
81
|
+
const limited = historyLimitGaps((await store.chats.historySkips(account)).filter((skip) => skip.chatId === chatId));
|
|
82
|
+
return { next: [...limited.next, ...held.next], missing: [...limited.missing, ...held.missing] };
|
|
83
|
+
})
|
|
80
84
|
: { next: [], missing: [] };
|
|
81
85
|
renderAnswer(this, context, listOf("messages list", {
|
|
82
86
|
items: messageItems(heardItems(page.items, hearing), reader),
|
|
@@ -12,6 +12,7 @@ import { renderAnswer } from "../envelope.js";
|
|
|
12
12
|
import { positiveCount } from "../paging.js";
|
|
13
13
|
import { archiveSummary } from "./archive-summary.js";
|
|
14
14
|
import { messengerContext } from "./context.js";
|
|
15
|
+
import { historySkipsOf } from "./history-gaps.js";
|
|
15
16
|
import { meaningNote, messagesAnswer, offsetOf, shownOf } from "./search-answers.js";
|
|
16
17
|
import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
|
|
17
18
|
import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
|
|
@@ -207,7 +208,10 @@ const searchAction = (messenger) => async function (given) {
|
|
|
207
208
|
...(newest ? { newest: true } : {}),
|
|
208
209
|
...(timezone === undefined ? {} : { timezone }),
|
|
209
210
|
};
|
|
210
|
-
|
|
211
|
+
const skips = await context
|
|
212
|
+
.withStore((store) => historySkipsOf(store, found.coverage?.accounts ?? found.accounts ?? []))
|
|
213
|
+
.catch(() => []);
|
|
214
|
+
renderAnswer(this, context, messagesAnswer("search messages", found, { limit, page: page ?? 1, again }, given.join(" "), skips), view);
|
|
211
215
|
};
|
|
212
216
|
const withOffset = (page, limit) => {
|
|
213
217
|
const offset = offsetOf(page, limit);
|
|
@@ -8,6 +8,7 @@ import type { MeaningSkipped } from "../../services/messages-discovery.js";
|
|
|
8
8
|
import type { FoundNote, NotesFoundOf } from "../../services/notes-search.js";
|
|
9
9
|
import type { SearchAllItem } from "../../services/search-all.js";
|
|
10
10
|
import type { MeetingSearchItem, SearchAllIncludingMeetingsFound } from "../../services/search-all-meetings.js";
|
|
11
|
+
import type { StoredHistorySkip } from "../../store/store.js";
|
|
11
12
|
import { type ListBody } from "../envelope.js";
|
|
12
13
|
/** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
|
|
13
14
|
export declare const SOURCE_SEARCHED: readonly ["messages", "notes", "events", "meetings"];
|
|
@@ -42,10 +43,10 @@ export interface Paging {
|
|
|
42
43
|
/** "showing 20 of 144", for the human view under a page of hits; `+` when the total is a lower bound. */
|
|
43
44
|
export declare const shownOf: (shown: number, offset: number, total: SearchTotal | undefined) => string | undefined;
|
|
44
45
|
/** `search messages` and `search mail`, CLI and MCP alike. */
|
|
45
|
-
export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string) => ListBody<MessageHitItem>;
|
|
46
|
+
export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string, skips?: StoredHistorySkip[]) => ListBody<MessageHitItem>;
|
|
46
47
|
export declare const noteHitItem: (hit: FoundNote) => NoteHitItem;
|
|
47
48
|
export declare const allHitItem: (item: SearchAllItem | MeetingSearchItem) => HitItem;
|
|
48
49
|
/** `search notes`, CLI and MCP alike. */
|
|
49
50
|
export declare const notesAnswer: (command: string, found: NotesFoundOf, { limit, page, again }: Paging, exact?: boolean) => ListBody<NoteHitItem>;
|
|
50
51
|
/** `search all`, CLI and MCP alike. A bounded meeting scan that could not tell whether more match says so. */
|
|
51
|
-
export declare const allAnswer: (command: string, found: SearchAllIncludingMeetingsFound, { limit, page, again }: Paging, exact?: boolean) => ListBody<HitItem>;
|
|
52
|
+
export declare const allAnswer: (command: string, found: SearchAllIncludingMeetingsFound, { limit, page, again }: Paging, exact?: boolean, skips?: StoredHistorySkip[]) => ListBody<HitItem>;
|
|
@@ -7,6 +7,7 @@ import { MAX_SEARCH_OFFSET } from "../../services/messages-archive-search.js";
|
|
|
7
7
|
import { listOf } from "../envelope.js";
|
|
8
8
|
import { nextStep } from "../steps.js";
|
|
9
9
|
import { archiveSummary } from "./archive-summary.js";
|
|
10
|
+
import { historyLimitGaps } from "./history-gaps.js";
|
|
10
11
|
/** What a source tool's `search all` reads: never mail, which is memo's (docs/dev/TOOL-BOUNDARIES.md). */
|
|
11
12
|
export const SOURCE_SEARCHED = ["messages", "notes", "events", "meetings"];
|
|
12
13
|
const nonEmpty = (list) => (list.length > 0 ? list : undefined);
|
|
@@ -249,8 +250,10 @@ export const shownOf = (shown, offset, total) => total === undefined || shown ==
|
|
|
249
250
|
? undefined
|
|
250
251
|
: `showing ${offset + 1}–${offset + shown} of ${total.value}${total.relation === "gte" ? "+" : ""}`;
|
|
251
252
|
/** `search messages` and `search mail`, CLI and MCP alike. */
|
|
252
|
-
export const messagesAnswer = (command, found, { limit, page, again }, query = "") => {
|
|
253
|
+
export const messagesAnswer = (command, found, { limit, page, again }, query = "", skips = []) => {
|
|
253
254
|
const accounts = found.coverage?.accounts ?? found.accounts ?? [];
|
|
255
|
+
const gaps = gapsOf(found);
|
|
256
|
+
const limited = historyLimitGaps(skips);
|
|
254
257
|
return listOf(command, {
|
|
255
258
|
items: [...found.items.map((hit) => messageHitItem(hit, query)), ...pictureItems(found)],
|
|
256
259
|
limit,
|
|
@@ -258,8 +261,9 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
|
|
|
258
261
|
page: page ?? 1,
|
|
259
262
|
...(found.total ? { total: found.total } : {}),
|
|
260
263
|
...(again ? { args: again } : {}),
|
|
261
|
-
...
|
|
262
|
-
|
|
264
|
+
next: [...gaps.next, ...limited.next],
|
|
265
|
+
missing: [...gaps.missing, ...limited.missing],
|
|
266
|
+
meta: metaOf(accounts, found.skipped, coverageGaps(found).missing.length > 0 || skips.length > 0),
|
|
263
267
|
});
|
|
264
268
|
};
|
|
265
269
|
/**
|
|
@@ -327,7 +331,8 @@ export const notesAnswer = (command, found, { limit, page, again }, exact = fals
|
|
|
327
331
|
meta: metaOf(found.accounts, found.skipped),
|
|
328
332
|
});
|
|
329
333
|
/** `search all`, CLI and MCP alike. A bounded meeting scan that could not tell whether more match says so. */
|
|
330
|
-
export const allAnswer = (command, found, { limit, page, again }, exact = false) => {
|
|
334
|
+
export const allAnswer = (command, found, { limit, page, again }, exact = false, skips = []) => {
|
|
335
|
+
const limited = historyLimitGaps(skips);
|
|
331
336
|
const scan = found.meetings?.complete === false ? found.meetings : undefined;
|
|
332
337
|
const notes = meaningGap(found.notes?.meaningSkipped, exact);
|
|
333
338
|
const messages = messageMeaningGap(found.messages?.meaningSkipped, "found_by");
|
|
@@ -351,6 +356,7 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
|
|
|
351
356
|
...notes.next,
|
|
352
357
|
...messages.next.filter((step) => !notes.next.some((asked) => JSON.stringify(asked) === JSON.stringify(step))),
|
|
353
358
|
...archive.next,
|
|
359
|
+
...limited.next,
|
|
354
360
|
],
|
|
355
361
|
missing: [
|
|
356
362
|
...(scan
|
|
@@ -365,7 +371,8 @@ export const allAnswer = (command, found, { limit, page, again }, exact = false)
|
|
|
365
371
|
...notes.missing,
|
|
366
372
|
...messages.missing,
|
|
367
373
|
...archive.missing,
|
|
374
|
+
...limited.missing,
|
|
368
375
|
],
|
|
369
|
-
meta: metaOf(found.accounts, found.skipped, archive.missing.length > 0),
|
|
376
|
+
meta: metaOf(found.accounts, found.skipped, archive.missing.length > 0 || skips.length > 0),
|
|
370
377
|
});
|
|
371
378
|
};
|
|
@@ -10,6 +10,7 @@ import { runsSearchCommand } from "../runs/search-command.js";
|
|
|
10
10
|
import { commandAt } from "../steps.js";
|
|
11
11
|
import { messengerContext } from "./context.js";
|
|
12
12
|
import { conversationsSearchCommand } from "./conversations-command.js";
|
|
13
|
+
import { historySkipsOf } from "./history-gaps.js";
|
|
13
14
|
import { messagesSearchCommand } from "./messages-search-command.js";
|
|
14
15
|
import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, shownOf, } from "./search-answers.js";
|
|
15
16
|
import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
|
|
@@ -92,7 +93,8 @@ const allCommand = (messenger) => {
|
|
|
92
93
|
...(exact ? { exact: true } : {}),
|
|
93
94
|
...(timezone === undefined ? {} : { timezone }),
|
|
94
95
|
};
|
|
95
|
-
|
|
96
|
+
const skips = await context.withStore((store) => historySkipsOf(store, found.accounts)).catch(() => []);
|
|
97
|
+
renderAnswer(this, context, allAnswer("search all", found, { limit, page: page ?? 1, again }, false, skips), () => {
|
|
96
98
|
if (found.items.length === 0)
|
|
97
99
|
context.renderer.note(found.server && !found.server.skipped
|
|
98
100
|
? "nothing found in the local store or on the messenger's server"
|
|
@@ -2,6 +2,7 @@ import type { Id } from "../../domain/models.js";
|
|
|
2
2
|
import { type MessengerAdapter } from "../../ports/messenger.js";
|
|
3
3
|
import type { MessengerTraits } from "../../ports/traits.js";
|
|
4
4
|
import type { EventSink, Warn } from "../../runs/events.js";
|
|
5
|
+
import { type HistoryLimits } from "../../services/history-limits.js";
|
|
5
6
|
import type { AccountKey, DeletionScope, MessageStore } from "../../store/store.js";
|
|
6
7
|
export interface Saving {
|
|
7
8
|
account: AccountKey;
|
|
@@ -41,6 +42,10 @@ export interface Saving {
|
|
|
41
42
|
namesFromStore?: Pick<MessengerTraits, "savedChatId">;
|
|
42
43
|
/** `MessengerTraits.unreadCounts: "store"`: a message `watch` or `listen` saves raises its chat's unread count. */
|
|
43
44
|
countsUnread?: boolean;
|
|
45
|
+
/** Pushed history (`feed`, `listenHistory`) outside these is not saved, and each chat records what it left out. */
|
|
46
|
+
historyLimits?: HistoryLimits;
|
|
47
|
+
/** The app command and profile, for the warning that names the command listing what the limits left out. */
|
|
48
|
+
command?: string;
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
46
51
|
* The adapter, with what each read answers written to the store. **A failed write never fails the
|
|
@@ -50,4 +55,4 @@ export interface Saving {
|
|
|
50
55
|
* would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
|
|
51
56
|
* known to be whole, and `saveMembers` replaces the list.
|
|
52
57
|
*/
|
|
53
|
-
export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, }: Saving) => MessengerAdapter;
|
|
58
|
+
export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, historyLimits, command, }: Saving) => MessengerAdapter;
|
|
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { capability, throughWrapper, } from "../../ports/messenger.js";
|
|
3
3
|
import { foldName, isId, pickPerson } from "../../resolve.js";
|
|
4
4
|
import { codeOf } from "../../sends/guarded.js";
|
|
5
|
+
import { limitBatch } from "../../services/history-limits.js";
|
|
5
6
|
import { chatIdIn } from "../../services/message-resolution.js";
|
|
6
7
|
const REKEY_PACING = { lockMs: 50, pauseMs: 25 };
|
|
7
8
|
/**
|
|
@@ -12,8 +13,37 @@ const REKEY_PACING = { lockMs: 50, pauseMs: 25 };
|
|
|
12
13
|
* would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
|
|
13
14
|
* known to be whole, and `saveMembers` replaces the list.
|
|
14
15
|
*/
|
|
15
|
-
export const stored = (messenger, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing = REKEY_PACING, closing, namesFromStore, recipientIds, countsUnread = false, }) => {
|
|
16
|
+
export const stored = (messenger, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing = REKEY_PACING, closing, namesFromStore, recipientIds, countsUnread = false, historyLimits, command = "cli", }) => {
|
|
16
17
|
let warned = false;
|
|
18
|
+
let limitedNoted = false;
|
|
19
|
+
const limited = async (opened, batch) => {
|
|
20
|
+
if (!historyLimits || !batch.messages?.length)
|
|
21
|
+
return { batch, skipped: new Map() };
|
|
22
|
+
const shapes = await opened.chats.historyShapes(account, [...new Set(batch.messages.map((one) => one.chatId))]);
|
|
23
|
+
return limitBatch(batch, (chatId) => shapes.get(chatId), (chatId) => shapes.get(chatId)?.included === true, historyLimits);
|
|
24
|
+
};
|
|
25
|
+
const noteLimited = (skipped) => {
|
|
26
|
+
if (skipped.size === 0)
|
|
27
|
+
return;
|
|
28
|
+
const counts = {
|
|
29
|
+
chats: skipped.size,
|
|
30
|
+
messages: 0,
|
|
31
|
+
group_too_large: 0,
|
|
32
|
+
older_than_window: 0,
|
|
33
|
+
};
|
|
34
|
+
for (const skip of skipped.values()) {
|
|
35
|
+
counts.messages = (counts.messages ?? 0) + skip.messages;
|
|
36
|
+
counts[skip.reason] = (counts[skip.reason] ?? 0) + 1;
|
|
37
|
+
}
|
|
38
|
+
const warning = { event: "warning", code: "history_limited", operation: "history.save", counts };
|
|
39
|
+
events(warning);
|
|
40
|
+
if (limitedNoted)
|
|
41
|
+
return;
|
|
42
|
+
limitedNoted = true;
|
|
43
|
+
warn(`history limits left out ${counts.messages} pushed messages of ${skipped.size} chats (big groups, older ` +
|
|
44
|
+
`than the window) — \`${command} store status\` lists them; \`${command} messages sync <chat> --full-history\` ` +
|
|
45
|
+
"fetches one and keeps it in full", warning);
|
|
46
|
+
};
|
|
17
47
|
let recorded = profile === undefined;
|
|
18
48
|
const notWritten = (operation, error) => {
|
|
19
49
|
const warning = { event: "warning", code: "store_not_written", operation };
|
|
@@ -155,7 +185,13 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
155
185
|
// A refused re-key leaves its chat split, never the others or the batch's history unsaved.
|
|
156
186
|
const saving = previous.then(async () => {
|
|
157
187
|
await saveRekeys(rekeys, round);
|
|
158
|
-
return attempt(operation, (opened) =>
|
|
188
|
+
return attempt(operation, async (opened) => {
|
|
189
|
+
const { batch: kept, skipped } = await limited(opened, batch);
|
|
190
|
+
await fill(opened, account, kept);
|
|
191
|
+
if (skipped.size > 0)
|
|
192
|
+
await opened.chats.recordHistorySkips(account, skipped);
|
|
193
|
+
noteLimited(skipped);
|
|
194
|
+
});
|
|
159
195
|
});
|
|
160
196
|
previous = saving;
|
|
161
197
|
pending?.add(saving);
|
package/dist/cli/settings.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type AISettings } from "../analysis/settings.js";
|
|
|
4
4
|
import { type AppIdentity } from "../ports/app.js";
|
|
5
5
|
import type { RecordInput } from "../runs/run-context.js";
|
|
6
6
|
import { type Level, type PermissionKey } from "../sends/permissions.js";
|
|
7
|
+
import { type HistoryLimits } from "../services/history-limits.js";
|
|
7
8
|
import { type ConfigIssue } from "./config-issues.js";
|
|
8
9
|
import type { CommandPermissions } from "./permission-keys.js";
|
|
9
10
|
export declare const plain: (rule: string) => (issue: v.BaseIssue<unknown>) => string;
|
|
@@ -94,6 +95,8 @@ export interface Settings extends AISettings {
|
|
|
94
95
|
color: boolean | undefined;
|
|
95
96
|
senderColors: boolean;
|
|
96
97
|
searchCatchUp?: boolean;
|
|
98
|
+
/** How much pushed history the store keeps, by chat kind; a messenger that answers history on request ignores it. */
|
|
99
|
+
historyLimits?: HistoryLimits;
|
|
97
100
|
/** Search and `images embed` read the pictures themselves, with a model on this machine. Off until set. */
|
|
98
101
|
pictureSearch?: boolean;
|
|
99
102
|
/** The reranker that orders `search messages --discover`'s first hits once it is downloaded, or `off`. */
|
|
@@ -300,6 +303,9 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
300
303
|
transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
301
304
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
302
305
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
306
|
+
history_dialog_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
307
|
+
history_group_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
308
|
+
history_group_members_max: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
303
309
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
304
310
|
search_reranker: v.OptionalSchema<v.PicklistSchema<[...string[], "off"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
305
311
|
update_check: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -376,6 +382,9 @@ export declare const settingsFor: <T extends object = Record<never, never>>(app:
|
|
|
376
382
|
transcribe_with: v.OptionalSchema<v.PicklistSchema<["auto", "messenger", "local"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
377
383
|
speech_model: v.OptionalSchema<v.StringSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
378
384
|
search_catch_up: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
385
|
+
history_dialog_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
386
|
+
history_group_days: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>, v.MaxValueAction<number, 1095, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
387
|
+
history_group_members_max: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<(issue: v.BaseIssue<unknown>) => string>, v.IntegerAction<number, (issue: v.BaseIssue<unknown>) => string>, v.MinValueAction<number, 1, (issue: v.BaseIssue<unknown>) => string>]>, undefined>;
|
|
379
388
|
picture_search: v.OptionalSchema<v.BooleanSchema<(issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
380
389
|
search_reranker: v.OptionalSchema<v.PicklistSchema<[...string[], "off"], (issue: v.BaseIssue<unknown>) => string>, undefined>;
|
|
381
390
|
}, (issue: v.StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
package/dist/cli/settings.js
CHANGED
|
@@ -7,6 +7,7 @@ import { DEFAULT_RERANK_MODEL, RERANK_MODELS } from "../embeddings/models.js";
|
|
|
7
7
|
import { envName, readOnlyLockOf } from "../ports/app.js";
|
|
8
8
|
import { KEEP_SUCCESSFUL_RUNS_FOR_DAYS, RUNS_SIZE_LIMIT_MB } from "../runs/run.js";
|
|
9
9
|
import { assertOverridesTighten, CEILING_KEY, COMMAND_PATH_KEY, LEVELS, layerPermissions, permissionOverrides, RESOURCE_KEY, RESOURCES, } from "../sends/permissions.js";
|
|
10
|
+
import { DEFAULT_HISTORY_LIMITS, HISTORY_MAX_DAYS } from "../services/history-limits.js";
|
|
10
11
|
import { storePath } from "../store/path.js";
|
|
11
12
|
import { sharedConfigPath } from "../store/shared-config.js";
|
|
12
13
|
import { checkConfigText, issueMessage, readConfigFile, readConfigText, savedPermissionKeys, schemaIssues, settingPathOf, withoutPath, } from "./config-issues.js";
|
|
@@ -34,6 +35,8 @@ export const count = v.pipe(v.number(wholeNumber), v.integer(wholeNumber), v.min
|
|
|
34
35
|
const zeroOrMore = plain("has to be a whole number, 0 or more");
|
|
35
36
|
const countOrZero = v.pipe(v.number(zeroOrMore), v.integer(zeroOrMore), v.minValue(0, zeroOrMore));
|
|
36
37
|
export const flag = v.boolean(plain("has to be true or false"));
|
|
38
|
+
const historyDaysMessage = plain(`has to be a whole number of days, 1 to ${HISTORY_MAX_DAYS}`);
|
|
39
|
+
const historyDays = v.pipe(v.number(historyDaysMessage), v.integer(historyDaysMessage), v.minValue(1, historyDaysMessage), v.maxValue(HISTORY_MAX_DAYS, historyDaysMessage));
|
|
37
40
|
/** `record_input`: true, false, or "failed" — the words typed kept only in a run that failed. */
|
|
38
41
|
export const recordInputSetting = v.union([v.boolean(), v.literal("failed")], plain('has to be true, false or "failed"'));
|
|
39
42
|
const wholeOrZero = (text, name) => {
|
|
@@ -71,6 +74,9 @@ const SHARED_PROFILE_ENTRIES = {
|
|
|
71
74
|
transcribe_with: v.optional(v.picklist(["auto", "messenger", "local"], plain("has to be auto, messenger or local"))),
|
|
72
75
|
speech_model: v.optional(v.string(plain("has to be a model id from `models list`, in quotes"))),
|
|
73
76
|
search_catch_up: v.optional(flag),
|
|
77
|
+
history_dialog_days: v.optional(historyDays),
|
|
78
|
+
history_group_days: v.optional(historyDays),
|
|
79
|
+
history_group_members_max: v.optional(count),
|
|
74
80
|
picture_search: v.optional(flag),
|
|
75
81
|
search_reranker: v.optional(v.picklist([...RERANK_MODELS.map((model) => model.id), "off"], plain(`has to be off or a reranker id: ${RERANK_MODELS.map((model) => model.id).join(", ")}`))),
|
|
76
82
|
...AI_ENTRIES,
|
|
@@ -91,6 +97,9 @@ export const MESSENGER_ONLY_SETTINGS = [
|
|
|
91
97
|
"sends_per_hour",
|
|
92
98
|
"transcribe_with",
|
|
93
99
|
"search_catch_up",
|
|
100
|
+
"history_dialog_days",
|
|
101
|
+
"history_group_days",
|
|
102
|
+
"history_group_members_max",
|
|
94
103
|
"read_other_bots",
|
|
95
104
|
];
|
|
96
105
|
/** One program, one version: whether to look for a newer one is not a per-profile matter. */
|
|
@@ -255,6 +264,9 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
255
264
|
const color = fromLayers("color", undefined);
|
|
256
265
|
const senderColors = fromLayers("sender_colors", false);
|
|
257
266
|
const searchCatchUp = fromLayers("search_catch_up", false);
|
|
267
|
+
const historyDialogDays = fromLayers("history_dialog_days", DEFAULT_HISTORY_LIMITS.dialogDays);
|
|
268
|
+
const historyGroupDays = fromLayers("history_group_days", DEFAULT_HISTORY_LIMITS.groupDays);
|
|
269
|
+
const historyGroupMembersMax = fromLayers("history_group_members_max", DEFAULT_HISTORY_LIMITS.groupMembersMax);
|
|
258
270
|
const pictureSearch = fromLayers("picture_search", false);
|
|
259
271
|
const searchReranker = fromLayers("search_reranker", DEFAULT_RERANK_MODEL);
|
|
260
272
|
const recordInput = fromLayers("record_input", true);
|
|
@@ -306,6 +318,11 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
306
318
|
color: color.value,
|
|
307
319
|
senderColors: senderColors.value,
|
|
308
320
|
searchCatchUp: searchCatchUp.value,
|
|
321
|
+
historyLimits: {
|
|
322
|
+
dialogDays: historyDialogDays.value,
|
|
323
|
+
groupDays: historyGroupDays.value,
|
|
324
|
+
groupMembersMax: historyGroupMembersMax.value,
|
|
325
|
+
},
|
|
309
326
|
pictureSearch: pictureSearch.value,
|
|
310
327
|
searchReranker: searchReranker.value,
|
|
311
328
|
limit: limit.value,
|
|
@@ -343,6 +360,9 @@ export const settingsFor = (app, extension = {}) => {
|
|
|
343
360
|
color: color.from,
|
|
344
361
|
senderColors: senderColors.from,
|
|
345
362
|
searchCatchUp: searchCatchUp.from,
|
|
363
|
+
historyDialogDays: historyDialogDays.from,
|
|
364
|
+
historyGroupDays: historyGroupDays.from,
|
|
365
|
+
historyGroupMembersMax: historyGroupMembersMax.from,
|
|
346
366
|
pictureSearch: pictureSearch.from,
|
|
347
367
|
searchReranker: searchReranker.from,
|
|
348
368
|
record: record.from,
|
|
@@ -82,6 +82,9 @@ export declare const appDoctorCommand: (app: import("./entry.js").AppIdentity, c
|
|
|
82
82
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
83
83
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
84
84
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
85
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
86
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
87
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
85
88
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
86
89
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
87
90
|
update_check: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -158,6 +161,9 @@ export declare const appDoctorCommand: (app: import("./entry.js").AppIdentity, c
|
|
|
158
161
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
159
162
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
160
163
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
164
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
165
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
166
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
161
167
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
162
168
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
163
169
|
}, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
|
@@ -256,6 +262,9 @@ export declare const appMcpCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
256
262
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
257
263
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
258
264
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
265
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
266
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
267
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
259
268
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
260
269
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
261
270
|
update_check: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -332,6 +341,9 @@ export declare const appMcpCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
332
341
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
333
342
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
334
343
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
344
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
345
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
346
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
335
347
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
336
348
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
337
349
|
}, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
|
@@ -437,6 +449,9 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
437
449
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
438
450
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
439
451
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
452
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
453
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
454
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
440
455
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
441
456
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
442
457
|
update_check: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -513,6 +528,9 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
|
|
|
513
528
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
514
529
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
515
530
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
531
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
532
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
533
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
516
534
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
517
535
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
518
536
|
}, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
|
@@ -646,6 +664,9 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
|
|
|
646
664
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
647
665
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
648
666
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
667
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
668
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
669
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
649
670
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
650
671
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
651
672
|
update_check: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
@@ -722,6 +743,9 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
|
|
|
722
743
|
transcribe_with: import("valibot").OptionalSchema<import("valibot").PicklistSchema<["auto", "messenger", "local"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
723
744
|
speech_model: import("valibot").OptionalSchema<import("valibot").StringSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
724
745
|
search_catch_up: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
746
|
+
history_dialog_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
747
|
+
history_group_days: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MaxValueAction<number, 1095, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
748
|
+
history_group_members_max: import("valibot").OptionalSchema<import("valibot").SchemaWithPipe<readonly [import("valibot").NumberSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").IntegerAction<number, (issue: import("valibot").BaseIssue<unknown>) => string>, import("valibot").MinValueAction<number, 1, (issue: import("valibot").BaseIssue<unknown>) => string>]>, undefined>;
|
|
725
749
|
picture_search: import("valibot").OptionalSchema<import("valibot").BooleanSchema<(issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
726
750
|
search_reranker: import("valibot").OptionalSchema<import("valibot").PicklistSchema<[...string[], "off"], (issue: import("valibot").BaseIssue<unknown>) => string>, undefined>;
|
|
727
751
|
}, (issue: import("valibot").StrictObjectIssue) => string>, "has to be an object of profiles, by name">, {}>;
|
package/dist/cli/steps.js
CHANGED
|
@@ -16,6 +16,7 @@ export const STEPS = defineSteps([
|
|
|
16
16
|
{ name: "all", required: false },
|
|
17
17
|
{ name: "since_time", required: false },
|
|
18
18
|
{ name: "background", required: false },
|
|
19
|
+
{ name: "full_history", required: false },
|
|
19
20
|
],
|
|
20
21
|
why: "the local copy is missing messages; syncing brings them in",
|
|
21
22
|
},
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -1106,6 +1106,7 @@ export declare const ENTITIES: {
|
|
|
1106
1106
|
readonly held: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
1107
1107
|
readonly held_chats: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
1108
1108
|
}, undefined>, undefined>;
|
|
1109
|
+
readonly history_included: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
1109
1110
|
}, undefined>, v.StrictObjectSchema<{
|
|
1110
1111
|
readonly batch: v.OptionalSchema<v.StrictObjectSchema<{
|
|
1111
1112
|
readonly attempted: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
@@ -209,6 +209,8 @@ export declare const messageSyncChat: v.StrictObjectSchema<{
|
|
|
209
209
|
readonly held: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
210
210
|
readonly held_chats: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
211
211
|
}, undefined>, undefined>;
|
|
212
|
+
/** `--full-history`: the chat's pushed history is now kept in full; `false` when the store does not know the chat. */
|
|
213
|
+
readonly history_included: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
212
214
|
}, undefined>;
|
|
213
215
|
export declare const messageSyncAll: v.StrictObjectSchema<{
|
|
214
216
|
readonly batch: v.OptionalSchema<v.StrictObjectSchema<{
|
|
@@ -333,6 +335,8 @@ export declare const messageSync: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
333
335
|
readonly held: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
334
336
|
readonly held_chats: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
335
337
|
}, undefined>, undefined>;
|
|
338
|
+
/** `--full-history`: the chat's pushed history is now kept in full; `false` when the store does not know the chat. */
|
|
339
|
+
readonly history_included: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
336
340
|
}, undefined>, v.StrictObjectSchema<{
|
|
337
341
|
readonly batch: v.OptionalSchema<v.StrictObjectSchema<{
|
|
338
342
|
readonly attempted: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
@@ -60,6 +60,8 @@ export const messageSyncChat = v.strictObject({
|
|
|
60
60
|
reached_last: v.optional(v.literal(true)),
|
|
61
61
|
stopped: v.optional(v.literal(true)),
|
|
62
62
|
tasks,
|
|
63
|
+
/** `--full-history`: the chat's pushed history is now kept in full; `false` when the store does not know the chat. */
|
|
64
|
+
history_included: v.optional(v.boolean()),
|
|
63
65
|
});
|
|
64
66
|
export const messageSyncAll = v.strictObject({
|
|
65
67
|
batch: v.optional(batchResult),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Id } from "../domain/models.js";
|
|
2
|
+
import type { HistoryBatch } from "../ports/messenger.js";
|
|
3
|
+
/** How much pushed history the store keeps of each kind of chat. */
|
|
4
|
+
export interface HistoryLimits {
|
|
5
|
+
/** One-to-one chats: days back from now. */
|
|
6
|
+
dialogDays: number;
|
|
7
|
+
/** Groups of more than two people: days back from now. */
|
|
8
|
+
groupDays: number;
|
|
9
|
+
/** Above this many people a group's pushed history is not kept at all. */
|
|
10
|
+
groupMembersMax: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const HISTORY_MAX_DAYS = 1095;
|
|
13
|
+
export declare const DEFAULT_HISTORY_LIMITS: HistoryLimits;
|
|
14
|
+
export type HistorySkipReason = "group_too_large" | "older_than_window";
|
|
15
|
+
/** What the limits left out of one chat, summed over every push. */
|
|
16
|
+
export interface HistorySkip {
|
|
17
|
+
reason: HistorySkipReason;
|
|
18
|
+
messages: number;
|
|
19
|
+
oldestAt: string;
|
|
20
|
+
newestAt: string;
|
|
21
|
+
/** For `group_too_large`: how many people the group had. */
|
|
22
|
+
members?: number;
|
|
23
|
+
/** For `older_than_window`: the window's start when the push was saved. */
|
|
24
|
+
keptFrom?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The kind and size of a chat, as the batch names it or the store holds it. */
|
|
27
|
+
export interface ChatShape {
|
|
28
|
+
kind: string;
|
|
29
|
+
participantsCount: number | null;
|
|
30
|
+
}
|
|
31
|
+
/** Whether this chat's pushed history is kept, and from when. A chat the owner lifted keeps everything. */
|
|
32
|
+
export declare const historyWindow: (shape: ChatShape | undefined, limits: HistoryLimits, now: number) => {
|
|
33
|
+
keepFrom: number;
|
|
34
|
+
} | {
|
|
35
|
+
skip: "group_too_large";
|
|
36
|
+
members: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The batch without the messages its chats' limits leave out, and what was left out per chat. Chat rows, people and
|
|
40
|
+
* re-keys always stay: a skipped chat is still listed, so its history can be fetched by hand.
|
|
41
|
+
*/
|
|
42
|
+
export declare const limitBatch: (batch: HistoryBatch, shapes: (chatId: Id) => ChatShape | undefined, included: (chatId: Id) => boolean, limits: HistoryLimits, now?: number) => {
|
|
43
|
+
batch: HistoryBatch;
|
|
44
|
+
skipped: Map<Id, HistorySkip>;
|
|
45
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const HISTORY_MAX_DAYS = 1095;
|
|
2
|
+
export const DEFAULT_HISTORY_LIMITS = { dialogDays: 1095, groupDays: 365, groupMembersMax: 50 };
|
|
3
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
/** Whether this chat's pushed history is kept, and from when. A chat the owner lifted keeps everything. */
|
|
5
|
+
export const historyWindow = (shape, limits, now) => {
|
|
6
|
+
const group = shape !== undefined && shape.kind !== "dialog" && shape.kind !== "saved";
|
|
7
|
+
if (group && (shape.participantsCount ?? 0) > limits.groupMembersMax)
|
|
8
|
+
return { skip: "group_too_large", members: shape.participantsCount ?? 0 };
|
|
9
|
+
const days = group ? limits.groupDays : limits.dialogDays;
|
|
10
|
+
return { keepFrom: now - Math.min(days, HISTORY_MAX_DAYS) * DAY_MS };
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The batch without the messages its chats' limits leave out, and what was left out per chat. Chat rows, people and
|
|
14
|
+
* re-keys always stay: a skipped chat is still listed, so its history can be fetched by hand.
|
|
15
|
+
*/
|
|
16
|
+
export const limitBatch = (batch, shapes, included, limits, now = Date.now()) => {
|
|
17
|
+
const skipped = new Map();
|
|
18
|
+
if (!batch.messages?.length)
|
|
19
|
+
return { batch, skipped };
|
|
20
|
+
const named = new Map((batch.chats ?? []).map((chat) => [chat.id, chat]));
|
|
21
|
+
const windows = new Map();
|
|
22
|
+
const windowOf = (chatId) => {
|
|
23
|
+
if (!windows.has(chatId)) {
|
|
24
|
+
const chat = named.get(chatId);
|
|
25
|
+
const shape = chat ? { kind: chat.kind, participantsCount: chat.participantsCount ?? null } : shapes(chatId);
|
|
26
|
+
windows.set(chatId, included(chatId) ? undefined : historyWindow(shape, limits, now));
|
|
27
|
+
}
|
|
28
|
+
return windows.get(chatId);
|
|
29
|
+
};
|
|
30
|
+
const messages = batch.messages.filter((message) => {
|
|
31
|
+
const window = windowOf(message.chatId);
|
|
32
|
+
if (window === undefined)
|
|
33
|
+
return true;
|
|
34
|
+
const at = Date.parse(message.timestamp);
|
|
35
|
+
if ("keepFrom" in window && !(at < window.keepFrom))
|
|
36
|
+
return true;
|
|
37
|
+
const reason = "skip" in window ? window.skip : "older_than_window";
|
|
38
|
+
const held = skipped.get(message.chatId);
|
|
39
|
+
const iso = new Date(at).toISOString();
|
|
40
|
+
skipped.set(message.chatId, {
|
|
41
|
+
reason,
|
|
42
|
+
messages: (held?.messages ?? 0) + 1,
|
|
43
|
+
oldestAt: held && held.oldestAt < iso ? held.oldestAt : iso,
|
|
44
|
+
newestAt: held && held.newestAt > iso ? held.newestAt : iso,
|
|
45
|
+
...("skip" in window ? { members: window.members } : { keptFrom: new Date(window.keepFrom).toISOString() }),
|
|
46
|
+
});
|
|
47
|
+
return false;
|
|
48
|
+
});
|
|
49
|
+
return { batch: { ...batch, messages }, skipped };
|
|
50
|
+
};
|
package/dist/store/chats.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ChannelTagMatch } from "../domain/channel-tags.js";
|
|
2
2
|
import type { Chat, ChatKind, ChatUpdate, Id, Page } from "../domain/models.js";
|
|
3
|
+
import type { HistorySkip } from "../services/history-limits.js";
|
|
3
4
|
import { type ChatRekey, type ChatRekeys } from "./sqlite/chat-rekey.js";
|
|
4
5
|
import type { ChatMetadata, LeftChat } from "./sqlite/chats.js";
|
|
5
6
|
import type { ChatCompleteness } from "./sqlite/completeness.js";
|
|
7
|
+
import type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
|
|
6
8
|
import type { StoreContext } from "./sqlite/open.js";
|
|
7
9
|
import type { Where } from "./sqlite/query-filter.js";
|
|
8
10
|
import type { AccountKey, ChatActivity, ChatStats, StoredChatFilter } from "./types.js";
|
|
@@ -64,6 +66,14 @@ export interface ChatsStore {
|
|
|
64
66
|
* `chatId` itself. Only for finding the chat: the old id and its history are never shown or searched.
|
|
65
67
|
*/
|
|
66
68
|
renamedChatId(key: AccountKey, chatId: Id): Promise<Id | undefined>;
|
|
69
|
+
/** Per stored chat: its kind, how many people, and whether the owner lifted its history limits. */
|
|
70
|
+
historyShapes(key: AccountKey, chatIds: Id[]): Promise<Map<Id, ChatHistoryShape>>;
|
|
71
|
+
/** Adds what the history limits left out of pushed history, per chat. */
|
|
72
|
+
recordHistorySkips(key: AccountKey, skips: Map<Id, HistorySkip>): Promise<void>;
|
|
73
|
+
/** Lifts the history limits for one chat, or puts them back; `false` when the store does not know the chat. */
|
|
74
|
+
setHistoryIncluded(key: AccountKey, chatId: Id, included: boolean): Promise<boolean>;
|
|
75
|
+
/** Every chat whose pushed history the limits left out, most messages first. */
|
|
76
|
+
historySkips(key: AccountKey): Promise<StoredHistorySkip[]>;
|
|
67
77
|
/** A stored chat's kind, with or without messages; `undefined` when the store does not know the chat. */
|
|
68
78
|
chatKind(key: AccountKey, chatId: Id): Promise<ChatKind | undefined>;
|
|
69
79
|
/** A stored chat's title, with or without messages; `null` when it has none or the store does not know the chat. */
|
package/dist/store/chats.js
CHANGED
|
@@ -3,6 +3,7 @@ import { keysOver } from "./keys.js";
|
|
|
3
3
|
import { rekeyChat, rekeyChats } from "./sqlite/chat-rekey.js";
|
|
4
4
|
import * as chatQueries from "./sqlite/chats.js";
|
|
5
5
|
import * as completeness from "./sqlite/completeness.js";
|
|
6
|
+
import * as history from "./sqlite/history-skips.js";
|
|
6
7
|
import * as reads from "./sqlite/reads.js";
|
|
7
8
|
import { write } from "./sqlite/transaction.js";
|
|
8
9
|
export const chatsStoreOver = (context) => {
|
|
@@ -85,6 +86,30 @@ export const chatsStoreOver = (context) => {
|
|
|
85
86
|
const accountKey = findAccountPk(key);
|
|
86
87
|
return accountKey === undefined ? undefined : chatQueries.renamedChatId(context, accountKey, chatId);
|
|
87
88
|
},
|
|
89
|
+
historyShapes: async (key, chatIds) => {
|
|
90
|
+
const accountKey = findAccountPk(key);
|
|
91
|
+
return accountKey === undefined ? new Map() : history.historyShapes(context, accountKey, chatIds);
|
|
92
|
+
},
|
|
93
|
+
recordHistorySkips: async (key, skips) => {
|
|
94
|
+
const accountKey = findAccountPk(key);
|
|
95
|
+
if (accountKey === undefined || skips.size === 0)
|
|
96
|
+
return;
|
|
97
|
+
write(database, () => history.recordHistorySkips(context, accountKey, skips));
|
|
98
|
+
},
|
|
99
|
+
setHistoryIncluded: async (key, chatId, included) => {
|
|
100
|
+
const accountKey = findAccountPk(key);
|
|
101
|
+
if (accountKey === undefined)
|
|
102
|
+
return false;
|
|
103
|
+
let done = false;
|
|
104
|
+
write(database, () => {
|
|
105
|
+
done = history.setHistoryIncluded(context, accountKey, chatId, included);
|
|
106
|
+
});
|
|
107
|
+
return done;
|
|
108
|
+
},
|
|
109
|
+
historySkips: async (key) => {
|
|
110
|
+
const accountKey = findAccountPk(key);
|
|
111
|
+
return accountKey === undefined ? [] : history.historySkips(context, accountKey);
|
|
112
|
+
},
|
|
88
113
|
chatKind: async (key, chatId) => {
|
|
89
114
|
const accountKey = findAccountPk(key);
|
|
90
115
|
if (accountKey === undefined)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { and, eq, inArray, or, sql } from "./drizzle/core.js";
|
|
3
3
|
import { folded } from "./folded.js";
|
|
4
|
+
import { withoutHistory } from "./history-skips.js";
|
|
4
5
|
import { identityPk } from "./identities.js";
|
|
5
6
|
import { bound } from "./query-filter.js";
|
|
6
7
|
import { chatEventPeople, chatEvents, chatMembers, chats, chunks, fetchLeases, identities, involvements, memberCounts, memberObservationMembers, memberObservations, memberStays, messageReactions, messageRevisions, messages, messageTranscripts, pollVotes, syncRanges, } from "./schema.js";
|
|
@@ -395,7 +396,7 @@ const toChat = (row, at, parentId) => ({
|
|
|
395
396
|
scope: row.scope,
|
|
396
397
|
parentChatId: parentId ?? null,
|
|
397
398
|
membershipState: row.membershipState,
|
|
398
|
-
providerMetadata: parsed(row.metadata),
|
|
399
|
+
providerMetadata: withoutHistory(parsed(row.metadata)),
|
|
399
400
|
}),
|
|
400
401
|
...muteOf(row, at),
|
|
401
402
|
...present({ archived: flag(row.archived), markedUnread: flag(row.markedUnread) }),
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Id } from "../../domain/models.js";
|
|
2
|
+
import type { ChatShape, HistorySkip } from "../../services/history-limits.js";
|
|
3
|
+
import type { StoreContext } from "./open.js";
|
|
4
|
+
/**
|
|
5
|
+
* The history limits' record of a chat, kept in the chat row's `metadata` under this key so no store version is
|
|
6
|
+
* needed; `Chat.providerMetadata` leaves it out.
|
|
7
|
+
*/
|
|
8
|
+
export declare const HISTORY_KEY = "$history";
|
|
9
|
+
export interface ChatHistoryShape extends ChatShape {
|
|
10
|
+
included: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface StoredHistorySkip extends HistorySkip {
|
|
13
|
+
chatId: Id;
|
|
14
|
+
title: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare const historyShapes: (context: StoreContext, accountKey: number, chatIds: Id[]) => Map<Id, ChatHistoryShape>;
|
|
17
|
+
/** Adds what one push left out to what the chat's record already holds. A chat the store lacks is left as it is. */
|
|
18
|
+
export declare const recordHistorySkips: (context: StoreContext, accountKey: number, skips: Map<Id, HistorySkip>) => void;
|
|
19
|
+
/** Lifts the limits for one chat from now on, or puts them back. Answers `false` for a chat the store lacks. */
|
|
20
|
+
export declare const setHistoryIncluded: (context: StoreContext, accountKey: number, chatId: Id, included: boolean) => boolean;
|
|
21
|
+
/** Every chat of the account whose pushed history the limits left out, most messages first. */
|
|
22
|
+
export declare const historySkips: ({ database }: StoreContext, accountKey: number) => StoredHistorySkip[];
|
|
23
|
+
/** `metadata` as the messenger gave it: the history record is the store's own. */
|
|
24
|
+
export declare const withoutHistory: <T extends Record<string, unknown> | undefined>(metadata: T) => T;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The history limits' record of a chat, kept in the chat row's `metadata` under this key so no store version is
|
|
3
|
+
* needed; `Chat.providerMetadata` leaves it out.
|
|
4
|
+
*/
|
|
5
|
+
export const HISTORY_KEY = "$history";
|
|
6
|
+
const objectOf = (text) => {
|
|
7
|
+
if (typeof text !== "string")
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(text);
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const recordOf = (metadata) => {
|
|
20
|
+
const value = objectOf(metadata)[HISTORY_KEY];
|
|
21
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
22
|
+
};
|
|
23
|
+
const rowOf = ({ database }, accountKey, chatId) => database
|
|
24
|
+
.prepare("SELECT id, kind, participants_count, metadata FROM chats WHERE account_id = ? AND external_id = ?")
|
|
25
|
+
.get(accountKey, chatId);
|
|
26
|
+
const update = (context, id, metadata, change) => {
|
|
27
|
+
const all = objectOf(metadata);
|
|
28
|
+
const next = change(recordOf(metadata));
|
|
29
|
+
if (next.skipped === undefined && !next.included)
|
|
30
|
+
delete all[HISTORY_KEY];
|
|
31
|
+
else
|
|
32
|
+
all[HISTORY_KEY] = next;
|
|
33
|
+
const text = Object.keys(all).length === 0 ? null : JSON.stringify(all);
|
|
34
|
+
context.database.prepare("UPDATE chats SET metadata = ?, updated_at = ? WHERE id = ?").run(text, context.now(), id);
|
|
35
|
+
};
|
|
36
|
+
export const historyShapes = (context, accountKey, chatIds) => {
|
|
37
|
+
const shapes = new Map();
|
|
38
|
+
for (const chatId of new Set(chatIds)) {
|
|
39
|
+
const row = rowOf(context, accountKey, chatId);
|
|
40
|
+
if (!row)
|
|
41
|
+
continue;
|
|
42
|
+
shapes.set(chatId, {
|
|
43
|
+
kind: String(row.kind),
|
|
44
|
+
participantsCount: row.participants_count == null ? null : Number(row.participants_count),
|
|
45
|
+
included: recordOf(row.metadata).included === true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return shapes;
|
|
49
|
+
};
|
|
50
|
+
/** Adds what one push left out to what the chat's record already holds. A chat the store lacks is left as it is. */
|
|
51
|
+
export const recordHistorySkips = (context, accountKey, skips) => {
|
|
52
|
+
for (const [chatId, skip] of skips) {
|
|
53
|
+
const row = rowOf(context, accountKey, chatId);
|
|
54
|
+
if (!row)
|
|
55
|
+
continue;
|
|
56
|
+
update(context, Number(row.id), row.metadata, (held) => {
|
|
57
|
+
const before = held.skipped;
|
|
58
|
+
const skipped = before
|
|
59
|
+
? {
|
|
60
|
+
...skip,
|
|
61
|
+
messages: before.messages + skip.messages,
|
|
62
|
+
oldestAt: before.oldestAt < skip.oldestAt ? before.oldestAt : skip.oldestAt,
|
|
63
|
+
newestAt: before.newestAt > skip.newestAt ? before.newestAt : skip.newestAt,
|
|
64
|
+
}
|
|
65
|
+
: skip;
|
|
66
|
+
return { ...held, skipped };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** Lifts the limits for one chat from now on, or puts them back. Answers `false` for a chat the store lacks. */
|
|
71
|
+
export const setHistoryIncluded = (context, accountKey, chatId, included) => {
|
|
72
|
+
const row = rowOf(context, accountKey, chatId);
|
|
73
|
+
if (!row)
|
|
74
|
+
return false;
|
|
75
|
+
update(context, Number(row.id), row.metadata, (held) => ({ ...held, included }));
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
/** Every chat of the account whose pushed history the limits left out, most messages first. */
|
|
79
|
+
export const historySkips = ({ database }, accountKey) => database
|
|
80
|
+
.prepare(`SELECT external_id, title, metadata FROM chats WHERE account_id = ? AND metadata LIKE '%"${HISTORY_KEY}"%'`)
|
|
81
|
+
.all(accountKey)
|
|
82
|
+
.flatMap((row) => {
|
|
83
|
+
const { skipped } = recordOf(row.metadata);
|
|
84
|
+
return skipped
|
|
85
|
+
? [{ chatId: String(row.external_id), title: row.title == null ? null : String(row.title), ...skipped }]
|
|
86
|
+
: [];
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => b.messages - a.messages);
|
|
89
|
+
/** `metadata` as the messenger gave it: the history record is the store's own. */
|
|
90
|
+
export const withoutHistory = (metadata) => {
|
|
91
|
+
if (metadata === undefined || !(HISTORY_KEY in metadata))
|
|
92
|
+
return metadata;
|
|
93
|
+
const { [HISTORY_KEY]: _, ...rest } = metadata;
|
|
94
|
+
return rest;
|
|
95
|
+
};
|
package/dist/store/store.d.ts
CHANGED
|
@@ -106,6 +106,7 @@ export interface MessageStore {
|
|
|
106
106
|
forAccount(key: AccountKey): AccountStore;
|
|
107
107
|
close(): Promise<void>;
|
|
108
108
|
}
|
|
109
|
+
export type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
|
|
109
110
|
export { BACKFILL_ON_OPEN, type StoreOptions } from "./sqlite/open-store.js";
|
|
110
111
|
export { fileFacts, readStoreTables, type TableKind } from "./sqlite/store-tables.js";
|
|
111
112
|
export declare const openStore: (options?: StoreOptions) => Promise<MessageStore>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.545.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -438,6 +438,9 @@
|
|
|
438
438
|
"--full": {
|
|
439
439
|
"meaning": "found messages with their whole text and every provider field"
|
|
440
440
|
},
|
|
441
|
+
"--full-history": {
|
|
442
|
+
"meaning": "keep this chat's pushed history in full from now on, past the history limits"
|
|
443
|
+
},
|
|
441
444
|
"--hidden": {
|
|
442
445
|
"value": "<on|off>",
|
|
443
446
|
"meaning": "on hides a forum's General topic from the topic list, off shows it"
|
|
@@ -3509,6 +3512,7 @@
|
|
|
3509
3512
|
"--catch-up-messages": "all",
|
|
3510
3513
|
"--catch-up-time": "all",
|
|
3511
3514
|
"--dry-run": "all",
|
|
3515
|
+
"--full-history": "all",
|
|
3512
3516
|
"--last": "all",
|
|
3513
3517
|
"--limit": "all",
|
|
3514
3518
|
"--no-catch-up": "all",
|