@wirecat/cli-core 0.366.0 → 0.368.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 +33 -0
- package/dist/cli/messenger/tasks-command.js +1 -1
- package/dist/context/evidence-response.js +8 -0
- package/dist/context/evidence.d.ts +11 -0
- package/dist/context/evidence.js +13 -1
- package/dist/services/archive.js +3 -3
- package/dist/services/attachments.js +3 -3
- package/dist/services/chat-documents.js +1 -1
- package/dist/services/chats.js +5 -5
- package/dist/services/conversations.js +1 -1
- package/dist/services/embeddings-maintenance.js +4 -4
- package/dist/services/embeddings-search.js +1 -1
- package/dist/services/evidence-read.js +1 -1
- package/dist/services/meeting-communications-preparation.js +4 -0
- package/dist/services/message-resolution.d.ts +7 -2
- package/dist/services/message-resolution.js +42 -18
- package/dist/services/messages.js +1 -1
- package/dist/services/metadata.js +3 -3
- package/dist/services/people.js +1 -1
- package/dist/services/pictures.js +2 -2
- package/dist/services/project-gathering-around.d.ts +8 -0
- package/dist/services/project-gathering-around.js +18 -0
- package/dist/services/project-gathering.d.ts +6 -0
- package/dist/services/project-gathering.js +58 -20
- package/dist/services/retention.js +1 -1
- package/dist/services/tags.js +1 -1
- package/dist/services/tasks.js +3 -3
- package/dist/store/sqlite/context-evidence.js +32 -23
- package/dist/store/sqlite/context-meeting-source.js +28 -18
- package/dist/store/sqlite/evidence-findings.js +21 -7
- package/dist/store/sqlite/findings-core.d.ts +1 -0
- package/dist/store/sqlite/findings-core.js +6 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.368.0 — 20.10.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Reads of a left chat's kept history find it by any id; live operations refuse it by any id.** `store export`
|
|
15
|
+
(also `--output-dir` with named chats), `store status`, `messages thread`, a search's `--chat` and `chat:` in one
|
|
16
|
+
account, `metadata get`, members, stats, events, tags, tasks, conversations, embeddings, pictures and attachments
|
|
17
|
+
now find a chat the account left by its name or id, the current one or one it was re-keyed from, as `chats show`
|
|
18
|
+
and `messages list` already did. Before, a left chat named by a non-numeric id was refused. `chats track` and
|
|
19
|
+
`store gaps plan` refuse a left chat with "you left chat <id>", also by a numeric id, which used to pass as typed.
|
|
20
|
+
A numeric id the store does not hold still passes as typed.
|
|
21
|
+
|
|
22
|
+
## 0.367.0 — 20.10.2026
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **A document hit in project gathering carries the paragraphs around it.** `around` holds, in document order, up
|
|
27
|
+
to `around.before` and `around.after` paragraphs next to the first one naming a searched word, plus that one
|
|
28
|
+
marked `found`. The bytes count as `usage.aroundBytes`, as a message's neighbours do.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **An evidence findings review accepts the findings whose own evidence is unchanged.** Before, one changed text
|
|
33
|
+
anywhere in a batch refused accepting any of it, so the agent had to stage the whole batch again. Now only the
|
|
34
|
+
accepted findings' project, assignee and cited texts are rechecked; the rest can be rejected in the same review.
|
|
35
|
+
Batches keep one hash per checked record for this; a batch staged earlier is still checked as a whole.
|
|
36
|
+
|
|
37
|
+
- **Project gathering moves past large documents instead of stalling.** A page of evidence now holds the records
|
|
38
|
+
that fit its byte budget together, rather than failing when they do not. A record larger than the page is read
|
|
39
|
+
alone, up to 256 KiB. One larger still is listed in the new `oversized` result field by reference and skipped,
|
|
40
|
+
so a resume no longer retries the same page and charges its budget each time. The context reader's page gains
|
|
41
|
+
the same optional `oversized`. Meeting preparation reports such a record as `read-limited` and continues past it.
|
|
42
|
+
|
|
10
43
|
## 0.366.0 — 20.10.2026
|
|
11
44
|
|
|
12
45
|
### Added
|
|
@@ -370,7 +370,7 @@ export const tasksCommand = (messenger) => {
|
|
|
370
370
|
const passes = await context.withStore(async (store, account) => {
|
|
371
371
|
const ids = [];
|
|
372
372
|
for (const chat of chats)
|
|
373
|
-
ids.push(await storedChatId(messenger, chat, store, account));
|
|
373
|
+
ids.push(await storedChatId(messenger, chat, store, account, { includeLeft: true }));
|
|
374
374
|
return applyTaskRulesToChats(store, account, ids, { ...(last === undefined ? {} : { last }), yes });
|
|
375
375
|
}, { name: "tasks scan" });
|
|
376
376
|
renderAnswer(this, context, listOf("tasks scan", { items: passes.map(snakeKeys), limit: passes.length, has_more: false }), () => `${passes.map((pass) => applyText(pass, yes, tool)).join("\n")}\n`);
|
|
@@ -34,6 +34,14 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
34
34
|
fail();
|
|
35
35
|
if (page.next)
|
|
36
36
|
contextEvidenceRequest({ ...request, cursor: page.next });
|
|
37
|
+
const big = page.oversized;
|
|
38
|
+
if (big !== undefined &&
|
|
39
|
+
(!big ||
|
|
40
|
+
page.items.length ||
|
|
41
|
+
typeof big.ref !== "string" ||
|
|
42
|
+
!Number.isSafeInteger(big.bytes) ||
|
|
43
|
+
big.bytes <= maxReadBytes))
|
|
44
|
+
fail();
|
|
37
45
|
let actual = 0;
|
|
38
46
|
for (const item of page.items) {
|
|
39
47
|
if (!item ||
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const CONTEXT_MAX_READ_BYTES = 262144;
|
|
1
2
|
export declare const CONTEXT_SOURCE_KINDS: readonly ["message", "mail", "document", "meeting"];
|
|
2
3
|
export interface ContextSource {
|
|
3
4
|
kind: (typeof CONTEXT_SOURCE_KINDS)[number];
|
|
@@ -40,6 +41,11 @@ export interface ContextEvidencePage {
|
|
|
40
41
|
scope: string;
|
|
41
42
|
} | null;
|
|
42
43
|
readBytes: number;
|
|
44
|
+
/** The next record alone is larger than maxReadBytes: it was not read, and next moves past it. */
|
|
45
|
+
oversized?: {
|
|
46
|
+
ref: string;
|
|
47
|
+
bytes: number;
|
|
48
|
+
};
|
|
43
49
|
coverage: {
|
|
44
50
|
available: boolean;
|
|
45
51
|
index: "ready" | "pending" | "missing" | "unknown";
|
|
@@ -65,6 +71,11 @@ export interface ContextIndex {
|
|
|
65
71
|
}
|
|
66
72
|
/** Changes whenever the cited text, its title or its revision does; a staged finding citing an older one is stale. */
|
|
67
73
|
export declare const contextEvidenceFingerprint: (item: Pick<ContextEvidenceItem, "ref" | "title" | "text" | "revision">) => string;
|
|
74
|
+
/** How many rows, in reading order, fit the allowance together; none when the first alone is larger. */
|
|
75
|
+
export declare const fittingRows: (sizes: readonly number[], limit: number, maxReadBytes: number) => {
|
|
76
|
+
count: number;
|
|
77
|
+
bytes: number;
|
|
78
|
+
};
|
|
68
79
|
/** One stored message, email or document as the context reader returns it, by its reference. */
|
|
69
80
|
export interface ContextRecordReader {
|
|
70
81
|
read(ref: string): Promise<Pick<ContextEvidenceItem, "ref" | "title" | "text" | "revision"> | null>;
|
package/dist/context/evidence.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
export const CONTEXT_MAX_READ_BYTES = 262144;
|
|
3
4
|
export const CONTEXT_SOURCE_KINDS = ["message", "mail", "document", "meeting"];
|
|
4
5
|
/** Changes whenever the cited text, its title or its revision does; a staged finding citing an older one is stale. */
|
|
5
6
|
export const contextEvidenceFingerprint = (item) => createHash("sha256")
|
|
6
7
|
.update(JSON.stringify(["context-evidence-v1", item.ref, item.title, item.text, item.revision]))
|
|
7
8
|
.digest("hex");
|
|
9
|
+
/** How many rows, in reading order, fit the allowance together; none when the first alone is larger. */
|
|
10
|
+
export const fittingRows = (sizes, limit, maxReadBytes) => {
|
|
11
|
+
let count = 0, bytes = 0;
|
|
12
|
+
for (const size of sizes.slice(0, limit)) {
|
|
13
|
+
if (!(bytes + size <= maxReadBytes))
|
|
14
|
+
break;
|
|
15
|
+
bytes += size;
|
|
16
|
+
count++;
|
|
17
|
+
}
|
|
18
|
+
return { count, bytes };
|
|
19
|
+
};
|
|
8
20
|
export const contextCursorScope = (request) => createHash("sha256")
|
|
9
21
|
.update(JSON.stringify([
|
|
10
22
|
"context-evidence-v1",
|
|
@@ -56,7 +68,7 @@ export const contextEvidenceRequest = (input) => {
|
|
|
56
68
|
if (!words.length || words.length > 16)
|
|
57
69
|
throw new CliError("validation_error", "query needs 1–16 literal words");
|
|
58
70
|
const limit = integer(input.limit ?? 10, "limit", 1, 20);
|
|
59
|
-
const maxReadBytes = integer(input.maxReadBytes ?? 65536, "maxReadBytes", 1,
|
|
71
|
+
const maxReadBytes = integer(input.maxReadBytes ?? 65536, "maxReadBytes", 1, CONTEXT_MAX_READ_BYTES);
|
|
60
72
|
if (input.cursor !== undefined && (!input.cursor || typeof input.cursor !== "object"))
|
|
61
73
|
throw new CliError("validation_error", "cursor needs at, id and scope");
|
|
62
74
|
if (input.cursor)
|
package/dist/services/archive.js
CHANGED
|
@@ -29,13 +29,13 @@ export const archiveService = (deps) => {
|
|
|
29
29
|
const found = async (chat) => {
|
|
30
30
|
const store = await deps.store();
|
|
31
31
|
const account = await deps.account();
|
|
32
|
-
return { store, account, chatId: await storedChatId(deps.messenger, chat, store, account) };
|
|
32
|
+
return { store, account, chatId: await storedChatId(deps.messenger, chat, store, account, { includeLeft: true }) };
|
|
33
33
|
};
|
|
34
34
|
return {
|
|
35
35
|
status: async (chat) => {
|
|
36
36
|
const store = await deps.store();
|
|
37
37
|
const account = await deps.account();
|
|
38
|
-
const only = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account);
|
|
38
|
+
const only = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
39
39
|
const stats = await store.chats.chatStats(account, only);
|
|
40
40
|
return Promise.all(stats.map(async (one) => ({ ...one, held: await store.sync.ranges(account, one.chatId) })));
|
|
41
41
|
},
|
|
@@ -54,7 +54,7 @@ export const archiveService = (deps) => {
|
|
|
54
54
|
const store = await deps.store();
|
|
55
55
|
const account = await deps.account();
|
|
56
56
|
if (chats !== undefined && chats.length > 0) {
|
|
57
|
-
const named = await Promise.all(chats.map((chat) => storedChatId(deps.messenger, chat, store, account)));
|
|
57
|
+
const named = await Promise.all(chats.map((chat) => storedChatId(deps.messenger, chat, store, account, { includeLeft: true })));
|
|
58
58
|
const titles = new Map((await store.chats.chatStats(account)).map((one) => [one.chatId, one.title]));
|
|
59
59
|
return { account, chats: [...new Set(named)].map((id) => ({ id, title: titles.get(id) ?? null })) };
|
|
60
60
|
}
|
|
@@ -147,7 +147,7 @@ const messageOf = async (deps, store, chat, message) => {
|
|
|
147
147
|
}
|
|
148
148
|
if (message === undefined)
|
|
149
149
|
throw new CliError("validation_error", "which message? give its id after the chat");
|
|
150
|
-
return { chatId: await chatIdIn(deps.messenger, chat, store), messageId: message.trim() };
|
|
150
|
+
return { chatId: await chatIdIn(deps.messenger, chat, store, { includeLeft: true }), messageId: message.trim() };
|
|
151
151
|
};
|
|
152
152
|
export const attachmentsService = (deps) => ({
|
|
153
153
|
extract: async ({ chat, message: onlyMessage, paths, fromDir, cursor, scanLimit: requestedScanLimit, limit: requestedLimit, download, onItem, signal, load = importEngine, ocr, concurrency = 1, } = {}) => {
|
|
@@ -167,7 +167,7 @@ export const attachmentsService = (deps) => ({
|
|
|
167
167
|
deps.guard.check({ chatId: null, key: "attachments.extract" }, { reserve: false });
|
|
168
168
|
const store = await accountStore(deps);
|
|
169
169
|
const { account } = store;
|
|
170
|
-
const chatId = chat === undefined ? undefined : await chatIdIn(deps.messenger, chat, store);
|
|
170
|
+
const chatId = chat === undefined ? undefined : await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
171
171
|
const directory = fromDir === undefined
|
|
172
172
|
? undefined
|
|
173
173
|
: await directoryPaths(store, chatId, fromDir, deps.messenger.app, deps.env ?? process.env);
|
|
@@ -346,7 +346,7 @@ export const attachmentsService = (deps) => ({
|
|
|
346
346
|
if (query)
|
|
347
347
|
for (const node of walkQuery(query.root))
|
|
348
348
|
if (node.field === "chat" && node.operator !== "wildcard" && !chats.has(node.value))
|
|
349
|
-
chats.set(node.value, await chatIdIn(deps.messenger, node.value, store));
|
|
349
|
+
chats.set(node.value, await chatIdIn(deps.messenger, node.value, store, { includeLeft: true }));
|
|
350
350
|
const where = query ? queryFilter(query, attachmentColumns(chats), { command: "attachments list" }) : undefined;
|
|
351
351
|
const views = await store.attachments.attachments({ ...(where ? { where } : {}), offset, limit: limit + 1 });
|
|
352
352
|
return { items: views.slice(0, limit).map((view) => itemOf(account, view)), has_more: views.length > limit };
|
|
@@ -5,7 +5,7 @@ import { chatIdIn } from "./message-resolution.js";
|
|
|
5
5
|
export const chatDocumentsService = (deps) => ({
|
|
6
6
|
list: async (chat, { limit, offset }) => {
|
|
7
7
|
const store = await accountStore(deps);
|
|
8
|
-
const chatId = await chatIdIn(deps.messenger, chat, store);
|
|
8
|
+
const chatId = await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
9
9
|
const items = (await store.messages.documentLinkMessages(chatId)).flatMap((message) => googleDocumentLinks(message.text).map(({ url, documentId }) => {
|
|
10
10
|
const stored = documentId === null ? undefined : message.documents.find((one) => one.fileId === documentId);
|
|
11
11
|
return {
|
package/dist/services/chats.js
CHANGED
|
@@ -24,7 +24,7 @@ export const CHAT_KINDS = ["dialog", "group", "channel", "saved"];
|
|
|
24
24
|
export const STORED_EVENTS = 1000;
|
|
25
25
|
/** For a messenger whose history is pushed: the events `serve` kept, never asking the messenger. */
|
|
26
26
|
const storedEvents = async (store, deps, chat, since) => {
|
|
27
|
-
const chatId = await chatIdIn(deps.messenger, chat, store);
|
|
27
|
+
const chatId = await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
28
28
|
const found = await store.roster.chatEvents(chatId, {
|
|
29
29
|
since: new Date(since).toISOString(),
|
|
30
30
|
limit: STORED_EVENTS + 1,
|
|
@@ -94,7 +94,7 @@ export const chatsService = (deps) => ({
|
|
|
94
94
|
return capability(connection, "members", "list a group's members")(chat, window);
|
|
95
95
|
}
|
|
96
96
|
const store = await accountStore(deps);
|
|
97
|
-
const chatId = await chatIdIn(deps.messenger, chat, store);
|
|
97
|
+
const chatId = await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
98
98
|
const everyone = await store.roster.members(chatId);
|
|
99
99
|
const end = window.limit === undefined ? everyone.length : window.offset + window.limit;
|
|
100
100
|
return { chatId, items: everyone.slice(window.offset, end), hasMore: everyone.length > end };
|
|
@@ -116,7 +116,7 @@ export const chatsService = (deps) => ({
|
|
|
116
116
|
},
|
|
117
117
|
stats: async (chat, { since, by, timezone }) => {
|
|
118
118
|
const store = await accountStore(deps);
|
|
119
|
-
const chatId = await chatIdIn(deps.messenger, chat, store);
|
|
119
|
+
const chatId = await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
120
120
|
const [completeness] = await store.chats.chatCompleteness([chatId]);
|
|
121
121
|
if (!completeness)
|
|
122
122
|
throw new CliError("not_found", `no stored chat ${chatId}`);
|
|
@@ -213,7 +213,7 @@ export const chatsService = (deps) => ({
|
|
|
213
213
|
tracked: async () => (await accountStore(deps)).roster.trackedChats(),
|
|
214
214
|
memberHistory: async (chat, { since }) => {
|
|
215
215
|
const store = await accountStore(deps);
|
|
216
|
-
const chatId = await chatIdIn(deps.messenger, chat, store);
|
|
216
|
+
const chatId = await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
217
217
|
const [stays, revisions] = await Promise.all([
|
|
218
218
|
store.roster.memberStays(chatId),
|
|
219
219
|
store.roster.profileRevisions(chatId),
|
|
@@ -222,7 +222,7 @@ export const chatsService = (deps) => ({
|
|
|
222
222
|
},
|
|
223
223
|
trackedChat: async (chat) => {
|
|
224
224
|
const store = await accountStore(deps);
|
|
225
|
-
const chatId = await chatIdIn(deps.messenger, chat, store);
|
|
225
|
+
const chatId = await chatIdIn(deps.messenger, chat, store, { includeLeft: true });
|
|
226
226
|
const entry = (await store.roster.trackedChats()).find((one) => one.chatId === chatId);
|
|
227
227
|
const since = new Date(Date.now() - TRACKED_DAYS * 86_400_000).toISOString().slice(0, 10);
|
|
228
228
|
return {
|
|
@@ -37,7 +37,7 @@ export const BATCH_SIZE = { default: 50, min: 10, max: 200 };
|
|
|
37
37
|
export const conversationsService = (deps) => {
|
|
38
38
|
const found = async (chat) => {
|
|
39
39
|
const store = await accountStore(deps);
|
|
40
|
-
return { store, chatId: await chatIdIn(deps.messenger, chat, store) };
|
|
40
|
+
return { store, chatId: await chatIdIn(deps.messenger, chat, store, { includeLeft: true }) };
|
|
41
41
|
};
|
|
42
42
|
const notBuilt = (chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations build --chat ${chatId}`)}\``);
|
|
43
43
|
return {
|
|
@@ -80,7 +80,7 @@ export const embeddingsMaintenance = (deps) => {
|
|
|
80
80
|
const found = async (chat) => {
|
|
81
81
|
const store = await deps.store();
|
|
82
82
|
const account = await deps.account();
|
|
83
|
-
const chatId = await storedChatId(deps.messenger, chat, store, account);
|
|
83
|
+
const chatId = await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
84
84
|
if (!(await store.conversations.conversationState(account, chatId))?.builtAt)
|
|
85
85
|
throw notBuiltError(deps, chatId);
|
|
86
86
|
return { store, account, chatId };
|
|
@@ -198,7 +198,7 @@ export const embeddingsMaintenance = (deps) => {
|
|
|
198
198
|
const store = await deps.store();
|
|
199
199
|
const account = await deps.account();
|
|
200
200
|
const target = embeddingTarget(deps, choice);
|
|
201
|
-
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account);
|
|
201
|
+
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
202
202
|
const chats = await readinessOf(deps, chatId, target.key);
|
|
203
203
|
if (chatId !== undefined)
|
|
204
204
|
return { model: target.id, chats };
|
|
@@ -211,7 +211,7 @@ export const embeddingsMaintenance = (deps) => {
|
|
|
211
211
|
const target = embeddingTarget(deps, choice);
|
|
212
212
|
if (embed && !build && !target.installed)
|
|
213
213
|
throw notDownloaded(deps, target.id);
|
|
214
|
-
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account);
|
|
214
|
+
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
215
215
|
const scan = async () => ({
|
|
216
216
|
chats: (await readinessOf(deps, chatId, target.key)).sort(byOldestBuild),
|
|
217
217
|
unbuilt: chatId === undefined ? await store.vectors.unbuiltGroups(account) : [],
|
|
@@ -278,7 +278,7 @@ export const embeddingsMaintenance = (deps) => {
|
|
|
278
278
|
clear: async (chat, choice) => {
|
|
279
279
|
const store = await deps.store();
|
|
280
280
|
const account = await deps.account();
|
|
281
|
-
const chatId = await storedChatId(deps.messenger, chat, store, account);
|
|
281
|
+
const chatId = await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
282
282
|
const cleared = await store.vectors.clearVectors(account, chatId, choice === undefined ? undefined : embeddingTarget(deps, choice).key);
|
|
283
283
|
return { chat: chatId, cleared };
|
|
284
284
|
},
|
|
@@ -235,7 +235,7 @@ export const embeddingsSearch = (deps) => {
|
|
|
235
235
|
const store = await deps.store();
|
|
236
236
|
const account = await deps.account();
|
|
237
237
|
const target = embeddingTarget(deps, choice);
|
|
238
|
-
const chatId = await storedChatId(deps.messenger, chat, store, account);
|
|
238
|
+
const chatId = await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
239
239
|
const [source] = await store.conversations.conversationsOfMessages(account, [{ chatId, messageId: message }]);
|
|
240
240
|
if (!source) {
|
|
241
241
|
if (!(await store.conversations.conversationState(account, chatId))?.builtAt)
|
|
@@ -7,7 +7,7 @@ export const readEvidencePacket = async (store, account, { chat, limit, before }
|
|
|
7
7
|
throw new CliError("validation_error", "evidence message limit must be an integer from 1 to 100");
|
|
8
8
|
if (before !== undefined && !before.trim())
|
|
9
9
|
throw new CliError("validation_error", "evidence before id must not be empty");
|
|
10
|
-
const chatId = await storedChatId(messenger, chat, store, account);
|
|
10
|
+
const chatId = await storedChatId(messenger, chat, store, account, { includeLeft: true });
|
|
11
11
|
const page = await store.messages.messages(account, chatId, { limit, ...(before === undefined ? {} : { before }) });
|
|
12
12
|
const items = page.items.toReversed();
|
|
13
13
|
const packet = prepareEvidencePacket({
|
|
@@ -41,6 +41,10 @@ export const readPreparationCommunications = async (plan, signal) => {
|
|
|
41
41
|
coverage.state = "unavailable";
|
|
42
42
|
coverage.issue = "not_found";
|
|
43
43
|
}
|
|
44
|
+
else if (page.oversized) {
|
|
45
|
+
coverage.state = "read-limited";
|
|
46
|
+
coverage.issue = "validation_error";
|
|
47
|
+
}
|
|
44
48
|
else if (page.coverage.index !== "ready")
|
|
45
49
|
coverage.issue = "configuration_error";
|
|
46
50
|
const refs = new Set();
|
|
@@ -10,8 +10,13 @@ export type Saved = Partial<Pick<MessengerTraits, "savedChatId" | "app">>;
|
|
|
10
10
|
* in store mode the store is the only source, so an empty answer would pass for "nothing was said".
|
|
11
11
|
*/
|
|
12
12
|
export declare const readChatId: (deps: ServiceDeps, reference: string, store: MessageStore, account: AccountKey) => Promise<string>;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A chat as typed, found among the stored chats the way an adapter finds it among its own; `includeLeft` tries the
|
|
15
|
+
* left ones last, for a read of their kept history.
|
|
16
|
+
*/
|
|
17
|
+
export declare const storedChatId: (messenger: Saved, reference: string, store: MessageStore, account: AccountKey, { includeLeft }?: {
|
|
18
|
+
includeLeft?: boolean;
|
|
19
|
+
}) => Promise<string>;
|
|
15
20
|
/** `storedChatId`, among the chats of the account the store is bound to; `includeLeft` tries the left ones last. */
|
|
16
21
|
export declare const chatIdIn: (messenger: Saved, reference: string, store: AccountStore, { includeLeft }?: {
|
|
17
22
|
includeLeft?: boolean;
|
|
@@ -22,24 +22,37 @@ export const readChatId = async (deps, reference, store, account) => {
|
|
|
22
22
|
return chatId;
|
|
23
23
|
};
|
|
24
24
|
const LEFT_EMPTY = "you left this chat, and the store holds none of its messages";
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
held: () => store.chats.chats(account, { includeLeft: true }),
|
|
36
|
-
renamed: (id) => store.chats.renamedChatId(account, id),
|
|
25
|
+
/**
|
|
26
|
+
* A chat as typed, found among the stored chats the way an adapter finds it among its own; `includeLeft` tries the
|
|
27
|
+
* left ones last, for a read of their kept history.
|
|
28
|
+
*/
|
|
29
|
+
export const storedChatId = (messenger, reference, store, account, { includeLeft = false } = {}) => {
|
|
30
|
+
const renamed = (id) => store.chats.renamedChatId(account, id);
|
|
31
|
+
const among = chatIdAmong(messenger, reference, account, {
|
|
32
|
+
held: () => store.chats.chats(account, {}),
|
|
33
|
+
renamed,
|
|
34
|
+
left: async (id) => leftIn(await store.chats.chats(account, byId(id))),
|
|
37
35
|
});
|
|
38
|
-
|
|
36
|
+
if (!includeLeft)
|
|
37
|
+
return among;
|
|
38
|
+
return among.catch((error) => {
|
|
39
|
+
if (codeOf(error) !== "not_found")
|
|
40
|
+
throw error;
|
|
41
|
+
return chatIdAmong(messenger, reference, account, {
|
|
42
|
+
held: () => store.chats.chats(account, { includeLeft: true }),
|
|
43
|
+
renamed,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const keptChatId = (messenger, reference, store, account) => storedChatId(messenger, reference, store, account, { includeLeft: true });
|
|
39
48
|
/** `storedChatId`, among the chats of the account the store is bound to; `includeLeft` tries the left ones last. */
|
|
40
49
|
export const chatIdIn = (messenger, reference, store, { includeLeft = false } = {}) => {
|
|
41
50
|
const renamed = (id) => store.chats.renamedChatId(id);
|
|
42
|
-
const among = chatIdAmong(messenger, reference, store.account, {
|
|
51
|
+
const among = chatIdAmong(messenger, reference, store.account, {
|
|
52
|
+
held: () => store.chats.chats({}),
|
|
53
|
+
renamed,
|
|
54
|
+
left: async (id) => leftIn(await store.chats.chats(byId(id))),
|
|
55
|
+
});
|
|
43
56
|
if (!includeLeft)
|
|
44
57
|
return among;
|
|
45
58
|
return among.catch((error) => {
|
|
@@ -51,13 +64,24 @@ export const chatIdIn = (messenger, reference, store, { includeLeft = false } =
|
|
|
51
64
|
});
|
|
52
65
|
});
|
|
53
66
|
};
|
|
54
|
-
const
|
|
67
|
+
const byId = (id) => ({
|
|
68
|
+
includeLeft: true,
|
|
69
|
+
limit: 1,
|
|
70
|
+
where: { sql: "chats.external_id = ?", params: [id] },
|
|
71
|
+
});
|
|
72
|
+
const leftIn = ({ items }) => items.some((one) => one.membershipState === "left");
|
|
73
|
+
/** `left` is given where a left chat is refused; it lets a numeric id the store does not hold through as typed. */
|
|
74
|
+
const chatIdAmong = async (messenger, reference, account, { held, renamed, left, }) => {
|
|
55
75
|
const trimmed = reference.trim();
|
|
56
76
|
if (messenger.savedChatId && ["me", "self", "saved"].includes(trimmed.toLowerCase())) {
|
|
57
77
|
return messenger.savedChatId(account);
|
|
58
78
|
}
|
|
59
|
-
if (/^-?\d+$/.test(trimmed))
|
|
60
|
-
|
|
79
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
80
|
+
const id = (await renamed(trimmed)) ?? trimmed;
|
|
81
|
+
if (await left?.(id))
|
|
82
|
+
throw new CliError("not_found", `you left chat ${id}`);
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
61
85
|
const chats = (await held()).items;
|
|
62
86
|
const exact = chats.find((one) => one.id === trimmed);
|
|
63
87
|
if (exact)
|
|
@@ -90,7 +114,7 @@ const missing = (error) => {
|
|
|
90
114
|
export const chatAmong = async (messenger, store, accounts, reference) => {
|
|
91
115
|
const [only] = accounts;
|
|
92
116
|
if (accounts.length === 1 && only)
|
|
93
|
-
return { account: only, chatId: await storedChatId(messenger, reference, store, only) };
|
|
117
|
+
return { account: only, chatId: await storedChatId(messenger, reference, store, only, { includeLeft: true }) };
|
|
94
118
|
const found = (await Promise.all(accounts.map(async (account) => {
|
|
95
119
|
const held = (await store.chats.chats(account, {})).items;
|
|
96
120
|
const chatId = await storedChatId(messenger, reference, store, account).catch(missing);
|
|
@@ -158,7 +158,7 @@ export const messagesService = (deps) => {
|
|
|
158
158
|
if (account.provider !== deps.messenger.provider ||
|
|
159
159
|
(target.account !== undefined && target.account !== account.account))
|
|
160
160
|
throw new CliError("validation_error", "that locator belongs to another account; select its profile first");
|
|
161
|
-
return readThreadContext(store, account, await storedChatId(deps.messenger, target.chat, store, account), target.message, options);
|
|
161
|
+
return readThreadContext(store, account, await storedChatId(deps.messenger, target.chat, store, account, { includeLeft: true }), target.message, options);
|
|
162
162
|
}),
|
|
163
163
|
comments: async (chat, typedPost, window) => {
|
|
164
164
|
const post = typedPost.trim();
|
|
@@ -3,10 +3,10 @@ import { CHANNEL_TAG_RULES_VERSION, classifyChannel } from "../domain/channel-ta
|
|
|
3
3
|
import { capability } from "../ports/messenger.js";
|
|
4
4
|
import { storedChatId } from "./message-resolution.js";
|
|
5
5
|
export const metadataService = (deps) => {
|
|
6
|
-
const held = async (reference) => {
|
|
6
|
+
const held = async (reference, { includeLeft = false } = {}) => {
|
|
7
7
|
const store = await deps.store();
|
|
8
8
|
const account = await deps.account();
|
|
9
|
-
const chatId = await storedChatId(deps.messenger, reference, store, account);
|
|
9
|
+
const chatId = await storedChatId(deps.messenger, reference, store, account, { includeLeft });
|
|
10
10
|
return { store, account, chatId };
|
|
11
11
|
};
|
|
12
12
|
const refresh = async (reference) => {
|
|
@@ -44,7 +44,7 @@ export const metadataService = (deps) => {
|
|
|
44
44
|
return missing;
|
|
45
45
|
},
|
|
46
46
|
get: async (reference) => {
|
|
47
|
-
const { store, account, chatId } = await held(reference);
|
|
47
|
+
const { store, account, chatId } = await held(reference, { includeLeft: true });
|
|
48
48
|
return { chatId, metadata: (await store.chats.chatMetadata(account, chatId)) ?? null };
|
|
49
49
|
},
|
|
50
50
|
refresh,
|
package/dist/services/people.js
CHANGED
|
@@ -80,7 +80,7 @@ export const peopleService = (deps) => {
|
|
|
80
80
|
const store = await deps.store();
|
|
81
81
|
const account = await deps.account();
|
|
82
82
|
if (!fetch) {
|
|
83
|
-
const ids = await Promise.all(chats.map((chat) => storedChatId(deps.messenger, chat, store, account)));
|
|
83
|
+
const ids = await Promise.all(chats.map((chat) => storedChatId(deps.messenger, chat, store, account, { includeLeft: true })));
|
|
84
84
|
return personMessages(store, account, person, {
|
|
85
85
|
chats: ids,
|
|
86
86
|
...(limit ? { limit } : {}),
|
|
@@ -80,7 +80,7 @@ export const picturesService = (deps) => {
|
|
|
80
80
|
throw new CliError("validation_error", PICTURES_OFF);
|
|
81
81
|
const store = await deps.store();
|
|
82
82
|
const account = await deps.account();
|
|
83
|
-
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account);
|
|
83
|
+
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
84
84
|
const use = await embedder();
|
|
85
85
|
const result = { model: model.key, embedded: 0, skipped: 0, failed: 0, more: false };
|
|
86
86
|
const pending = [];
|
|
@@ -177,7 +177,7 @@ export const picturesService = (deps) => {
|
|
|
177
177
|
return {};
|
|
178
178
|
const store = await deps.store();
|
|
179
179
|
const account = await deps.account();
|
|
180
|
-
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account);
|
|
180
|
+
const chatId = chat === undefined ? undefined : await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
181
181
|
let vector;
|
|
182
182
|
try {
|
|
183
183
|
const use = await embedder();
|
|
@@ -20,6 +20,14 @@ export interface ProjectNeighbourReader {
|
|
|
20
20
|
}[]>;
|
|
21
21
|
}
|
|
22
22
|
export declare const storedMessageNeighbours: (messages: Pick<MessagesStore, "around">) => ProjectNeighbourReader;
|
|
23
|
+
/** The paragraphs around the first one that names the query, in document order; the hit's own is marked found. */
|
|
24
|
+
export declare const documentPassage: (text: string, query: string, window: {
|
|
25
|
+
before: number;
|
|
26
|
+
after: number;
|
|
27
|
+
}) => {
|
|
28
|
+
text: string;
|
|
29
|
+
found: boolean;
|
|
30
|
+
}[];
|
|
23
31
|
/**
|
|
24
32
|
* Neighbours are context for reading a hit, not matches: they never join `items`, and their text counts against
|
|
25
33
|
* the run's read bytes.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { formatLocator, parseLocator } from "../domain/locator.js";
|
|
3
|
+
import { fold } from "../store/normalize.js";
|
|
3
4
|
import { excerpt } from "./excerpt.js";
|
|
4
5
|
export const storedMessageNeighbours = (messages) => ({
|
|
5
6
|
async around(ref, window) {
|
|
@@ -17,6 +18,23 @@ export const storedMessageNeighbours = (messages) => ({
|
|
|
17
18
|
}));
|
|
18
19
|
},
|
|
19
20
|
});
|
|
21
|
+
const wordsOf = (text) => (text.match(/[\p{L}\p{N}\p{M}]+/gu) ?? []).map(fold);
|
|
22
|
+
/** The paragraphs around the first one that names the query, in document order; the hit's own is marked found. */
|
|
23
|
+
export const documentPassage = (text, query, window) => {
|
|
24
|
+
const paragraphs = text
|
|
25
|
+
.split(/\n\s*\n/)
|
|
26
|
+
.map((part) => part.replace(/\s+/g, " ").trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const words = new Set(wordsOf(query));
|
|
29
|
+
const hit = paragraphs.findIndex((part) => wordsOf(part).some((word) => words.has(word)));
|
|
30
|
+
if (hit < 0 || paragraphs.length < 2)
|
|
31
|
+
return [];
|
|
32
|
+
const from = Math.max(0, hit - window.before);
|
|
33
|
+
return paragraphs.slice(from, hit + window.after + 1).map((part, index) => ({
|
|
34
|
+
text: excerpt(part, query, 200, { literal: true }),
|
|
35
|
+
found: from + index === hit,
|
|
36
|
+
}));
|
|
37
|
+
};
|
|
20
38
|
/**
|
|
21
39
|
* Neighbours are context for reading a hit, not matches: they never join `items`, and their text counts against
|
|
22
40
|
* the run's read bytes.
|
|
@@ -79,6 +79,12 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
|
|
|
79
79
|
issue: string | null;
|
|
80
80
|
}[];
|
|
81
81
|
items: ProjectEvidenceItem[];
|
|
82
|
+
oversized: {
|
|
83
|
+
ref: string;
|
|
84
|
+
lanes: string[];
|
|
85
|
+
source: number;
|
|
86
|
+
bytes: number;
|
|
87
|
+
}[];
|
|
82
88
|
matchedAgain: {
|
|
83
89
|
ref: string;
|
|
84
90
|
lanes: string[];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { contextEvidenceFingerprint } from "../context/evidence.js";
|
|
2
|
+
import { CONTEXT_MAX_READ_BYTES, contextEvidenceFingerprint } from "../context/evidence.js";
|
|
3
3
|
import { createContextEvidenceReader } from "./context-evidence.js";
|
|
4
4
|
import { excerpt } from "./excerpt.js";
|
|
5
|
-
import { attachNeighbours } from "./project-gathering-around.js";
|
|
5
|
+
import { attachNeighbours, documentPassage, } from "./project-gathering-around.js";
|
|
6
6
|
import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
|
|
7
7
|
const terminal = new Set(["done", "unavailable", "failed"]);
|
|
8
8
|
export const gatherProjectEvidence = async (ports, request) => {
|
|
@@ -72,6 +72,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
72
72
|
const items = [];
|
|
73
73
|
const byRef = new Map();
|
|
74
74
|
const matchedAgain = new Map();
|
|
75
|
+
const oversized = new Map();
|
|
75
76
|
let stopped = null;
|
|
76
77
|
const open = (u) => !terminal.has(u.state) && u.state !== "read-limited";
|
|
77
78
|
const take = (item, lane, kind, source) => {
|
|
@@ -101,6 +102,18 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
101
102
|
lanes: [lane.id],
|
|
102
103
|
context: lane.kinds.every((k) => k === "person") ? "person-only" : "project",
|
|
103
104
|
};
|
|
105
|
+
if (kind === "document" && plan.around.before + plan.around.after) {
|
|
106
|
+
entry.around = [];
|
|
107
|
+
for (const part of documentPassage(item.text, lane.query, plan.around)) {
|
|
108
|
+
const bytes = Buffer.byteLength(part.text);
|
|
109
|
+
if (bytes > plan.budgets.maxReadBytes - usage.readBytes - usage.aroundBytes) {
|
|
110
|
+
entry.aroundIssue = "read-bytes";
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
usage.aroundBytes += bytes;
|
|
114
|
+
entry.around.push({ ref: item.ref, at: item.at, sender: null, ...part });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
104
117
|
byRef.set(item.ref, entry);
|
|
105
118
|
items.push(entry);
|
|
106
119
|
usage.items++;
|
|
@@ -126,24 +139,49 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
126
139
|
: null;
|
|
127
140
|
if (stopped)
|
|
128
141
|
break;
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
usage.reads++;
|
|
135
|
-
try {
|
|
136
|
-
const page = await reader.read({
|
|
137
|
-
source: unit.source,
|
|
138
|
-
query: unit.lane.query,
|
|
139
|
-
after: unit.chunk.after,
|
|
140
|
-
before: unit.chunk.before,
|
|
141
|
-
limit: 20,
|
|
142
|
-
maxReadBytes: allowance,
|
|
143
|
-
...(state.cursor ? { cursor: state.cursor } : {}),
|
|
144
|
-
signal,
|
|
142
|
+
const readPage = async (allowance, limit) => {
|
|
143
|
+
const reader = createContextEvidenceReader(ports.evidence, [unit.source], {
|
|
144
|
+
maxReads: 1,
|
|
145
|
+
maxTotalReadBytes: allowance,
|
|
145
146
|
});
|
|
146
|
-
usage.
|
|
147
|
+
usage.reads++;
|
|
148
|
+
try {
|
|
149
|
+
const page = await reader.read({
|
|
150
|
+
source: unit.source,
|
|
151
|
+
query: unit.lane.query,
|
|
152
|
+
after: unit.chunk.after,
|
|
153
|
+
before: unit.chunk.before,
|
|
154
|
+
limit,
|
|
155
|
+
maxReadBytes: allowance,
|
|
156
|
+
...(state.cursor ? { cursor: state.cursor } : {}),
|
|
157
|
+
signal,
|
|
158
|
+
});
|
|
159
|
+
usage.readBytes += page.readBytes;
|
|
160
|
+
return page;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
usage.readBytes += reader.usage().chargedBytes;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
let page = await readPage(Math.min(plan.budgets.maxPageBytes, plan.budgets.maxReadBytes - usage.readBytes), 20);
|
|
169
|
+
const big = page.oversized;
|
|
170
|
+
if (big && big.bytes <= CONTEXT_MAX_READ_BYTES) {
|
|
171
|
+
if (big.bytes > plan.budgets.maxReadBytes - usage.readBytes || usage.reads >= plan.budgets.maxReads) {
|
|
172
|
+
state.state = "read-limited";
|
|
173
|
+
state.code = "validation_error";
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
page = await readPage(big.bytes, 1);
|
|
177
|
+
}
|
|
178
|
+
else if (big) {
|
|
179
|
+
const known = oversized.get(big.ref);
|
|
180
|
+
if (!known)
|
|
181
|
+
oversized.set(big.ref, { ref: big.ref, lanes: [unit.lane.id], source: unit.sourceIndex, bytes: big.bytes });
|
|
182
|
+
else if (!known.lanes.includes(unit.lane.id))
|
|
183
|
+
known.lanes.push(unit.lane.id);
|
|
184
|
+
}
|
|
147
185
|
state.pages++;
|
|
148
186
|
state.code = page.coverage.index === "ready" ? null : `index-${page.coverage.index}`;
|
|
149
187
|
for (const item of page.items)
|
|
@@ -161,7 +199,6 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
161
199
|
}
|
|
162
200
|
catch (error) {
|
|
163
201
|
signal?.throwIfAborted();
|
|
164
|
-
usage.readBytes += reader.usage().chargedBytes;
|
|
165
202
|
if (!(error instanceof CliError))
|
|
166
203
|
throw error;
|
|
167
204
|
if (error.code === "validation_error" && error.details?.reason === "context.read-budget")
|
|
@@ -225,6 +262,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
225
262
|
issue: progress[index]?.code ?? null,
|
|
226
263
|
})),
|
|
227
264
|
items,
|
|
265
|
+
oversized: [...oversized.values()],
|
|
228
266
|
matchedAgain: [...matchedAgain].map(([ref, lanes]) => ({ ref, lanes: [...lanes] })),
|
|
229
267
|
usage: { ...usage, elapsedMs: now() - started },
|
|
230
268
|
totals,
|
|
@@ -26,7 +26,7 @@ export const retentionService = (deps) => ({
|
|
|
26
26
|
limit: query.limit ?? 20,
|
|
27
27
|
};
|
|
28
28
|
const store = await deps.store(), account = await deps.account();
|
|
29
|
-
const chatId = await storedChatId(deps.messenger, chat, store, account);
|
|
29
|
+
const chatId = await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
30
30
|
const result = await store.roster.retention(account, chatId, options);
|
|
31
31
|
return {
|
|
32
32
|
...result,
|
package/dist/services/tags.js
CHANGED
|
@@ -71,7 +71,7 @@ const targetOf = async (deps, store, account, { chat, contact, message, task })
|
|
|
71
71
|
}
|
|
72
72
|
if (chat === undefined)
|
|
73
73
|
throw new CliError("validation_error", ONE_TARGET);
|
|
74
|
-
const chatId = await storedChatId(deps.messenger, chat, store, account);
|
|
74
|
+
const chatId = await storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
75
75
|
return message === undefined ? { type: "chat", chatId } : { type: "message", chatId, messageId: message.trim() };
|
|
76
76
|
};
|
|
77
77
|
const viewOf = (account, target) => target.type === "message"
|
package/dist/services/tasks.js
CHANGED
|
@@ -149,7 +149,7 @@ export const tasksService = (deps, categories) => {
|
|
|
149
149
|
const categoryOf = async (store, name) => taskCategoryOf(await listOf(store))(name);
|
|
150
150
|
const inStore = async (work) => work(await deps.store(), await deps.account());
|
|
151
151
|
const service = (store) => createTaskService({ store: store.tasks });
|
|
152
|
-
const groupOf = async (store, account, chat) => chat === undefined ? undefined : storedChatId(deps.messenger, chat, store, account);
|
|
152
|
+
const groupOf = async (store, account, chat) => chat === undefined ? undefined : storedChatId(deps.messenger, chat, store, account, { includeLeft: true });
|
|
153
153
|
const refuseBare = (target) => {
|
|
154
154
|
throw new CliError("validation_error", `no task ${singleLine(target)} — write task:${singleLine(target)} for a task`);
|
|
155
155
|
};
|
|
@@ -212,7 +212,7 @@ export const tasksService = (deps, categories) => {
|
|
|
212
212
|
const groups = new Map();
|
|
213
213
|
for (const node of query ? walkQuery(query.root) : [])
|
|
214
214
|
if (node.field === "chat" && node.operator !== "wildcard" && !groups.has(node.value))
|
|
215
|
-
groups.set(node.value, await storedChatId(deps.messenger, node.value, store, account));
|
|
215
|
+
groups.set(node.value, await storedChatId(deps.messenger, node.value, store, account, { includeLeft: true }));
|
|
216
216
|
const where = query ? queryFilter(query, taskColumns(groups), { command: "tasks list" }) : undefined;
|
|
217
217
|
const rows = await store.tasks.query(taskAccount(account), where, { limit: limit + 1, offset });
|
|
218
218
|
const shown = rows.slice(0, limit);
|
|
@@ -443,7 +443,7 @@ export const tasksService = (deps, categories) => {
|
|
|
443
443
|
type: "chat",
|
|
444
444
|
provider: account.provider,
|
|
445
445
|
account: account.account,
|
|
446
|
-
chat: await storedChatId(deps.messenger, chat, store, account),
|
|
446
|
+
chat: await storedChatId(deps.messenger, chat, store, account, { includeLeft: true }),
|
|
447
447
|
})));
|
|
448
448
|
const origin = by === "owner" ? "owner" : "suggested";
|
|
449
449
|
for (const target of [...written, ...chatRefs]) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { contextCursorScope, contextEvidenceRequest, } from "../../context/evidence.js";
|
|
1
|
+
import { contextCursorScope, contextEvidenceRequest, fittingRows, } from "../../context/evidence.js";
|
|
3
2
|
import { formatLocator, parseLocator } from "../../domain/locator.js";
|
|
4
3
|
import { NORMALIZER_VERSION, normalize } from "../normalize.js";
|
|
5
4
|
import { contextDocumentTime, contextDocumentTimeKind, contextDocumentUrl } from "./context-document-source.js";
|
|
@@ -122,38 +121,48 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
122
121
|
if (cursor)
|
|
123
122
|
params.push(cursor.at, cursor.at, cursor.id);
|
|
124
123
|
params.push(limit + 1);
|
|
125
|
-
const
|
|
126
|
-
.prepare(`SELECT
|
|
124
|
+
const sized = database
|
|
125
|
+
.prepare(`SELECT id,at,provider,account,chat,external_id,
|
|
127
126
|
length(CAST(coalesce(title,'') AS BLOB))+length(CAST(coalesce(body,'') AS BLOB))+
|
|
128
127
|
length(CAST(coalesce(url,'') AS BLOB))+length(CAST(provider AS BLOB))+
|
|
129
128
|
length(CAST(account AS BLOB))+length(CAST(external_id AS BLOB))+
|
|
130
|
-
length(CAST(coalesce(chat,'') AS BLOB))+length(CAST(time_kind AS BLOB))
|
|
131
|
-
.
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
length(CAST(coalesce(chat,'') AS BLOB))+length(CAST(time_kind AS BLOB)) AS bytes FROM (${selection})`)
|
|
130
|
+
.all(...params);
|
|
131
|
+
const refOf = (row) => source.kind === "document"
|
|
132
|
+
? `document:${row.id}`
|
|
133
|
+
: formatLocator({
|
|
134
|
+
provider: String(row.provider),
|
|
135
|
+
account: String(row.account),
|
|
136
|
+
chat: String(row.chat),
|
|
137
|
+
message: String(row.external_id),
|
|
136
138
|
});
|
|
139
|
+
const past = (row) => ({ at: Number(row.at), id: Number(row.id), scope });
|
|
140
|
+
const fit = fittingRows(sized.map((row) => Number(row.bytes)), limit, maxReadBytes);
|
|
141
|
+
const first = sized[0];
|
|
142
|
+
if (!fit.count && first) {
|
|
143
|
+
coverage.hasMore = sized.length > 1;
|
|
144
|
+
return {
|
|
145
|
+
source,
|
|
146
|
+
items: [],
|
|
147
|
+
readBytes: 0,
|
|
148
|
+
coverage,
|
|
149
|
+
next: coverage.hasMore ? past(first) : null,
|
|
150
|
+
oversized: { ref: refOf(first), bytes: Number(first.bytes) },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
137
153
|
signal?.throwIfAborted();
|
|
138
|
-
|
|
154
|
+
params[params.length - 1] = fit.count;
|
|
155
|
+
const selected = fit.count ? database.prepare(selection).all(...params) : [];
|
|
139
156
|
signal?.throwIfAborted();
|
|
140
|
-
coverage.hasMore =
|
|
141
|
-
const selected = rows.slice(0, limit);
|
|
157
|
+
coverage.hasMore = sized.length > selected.length;
|
|
142
158
|
const last = selected.at(-1);
|
|
143
159
|
return {
|
|
144
160
|
source,
|
|
145
|
-
readBytes,
|
|
161
|
+
readBytes: fit.bytes,
|
|
146
162
|
coverage,
|
|
147
|
-
next: coverage.hasMore && last ?
|
|
163
|
+
next: coverage.hasMore && last ? past(last) : null,
|
|
148
164
|
items: selected.map((row) => ({
|
|
149
|
-
ref:
|
|
150
|
-
? `document:${row.id}`
|
|
151
|
-
: formatLocator({
|
|
152
|
-
provider: String(row.provider),
|
|
153
|
-
account: String(row.account),
|
|
154
|
-
chat: String(row.chat),
|
|
155
|
-
message: String(row.external_id),
|
|
156
|
-
}),
|
|
165
|
+
ref: refOf(row),
|
|
157
166
|
provider: String(row.provider),
|
|
158
167
|
account: String(row.account),
|
|
159
168
|
title: row.title == null ? null : String(row.title),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { contextCursorScope } from "../../context/evidence.js";
|
|
1
|
+
import { contextCursorScope, fittingRows, } from "../../context/evidence.js";
|
|
3
2
|
import { normalize } from "../normalize.js";
|
|
4
3
|
import { read } from "./transaction.js";
|
|
5
4
|
/** Transcript cues keep their retained revision reference; a meeting title alone is not a transcript match. */
|
|
@@ -57,29 +56,40 @@ export const readMeetingContextEvidence = (database, request) => {
|
|
|
57
56
|
if (cursor)
|
|
58
57
|
params.push(cursor.at, cursor.at, cursor.id);
|
|
59
58
|
params.push(limit + 1);
|
|
60
|
-
const
|
|
61
|
-
.prepare(`SELECT
|
|
59
|
+
const sized = database
|
|
60
|
+
.prepare(`SELECT id,at,ref,length(CAST(coalesce(title,'') AS BLOB))+
|
|
62
61
|
length(CAST(text AS BLOB))+length(CAST(provider AS BLOB))+
|
|
63
|
-
length(CAST(account AS BLOB))+length(CAST(ref AS BLOB))+length('meeting-start')
|
|
64
|
-
.
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
length(CAST(account AS BLOB))+length(CAST(ref AS BLOB))+length('meeting-start') AS bytes FROM (${selection})`)
|
|
63
|
+
.all(...params);
|
|
64
|
+
const past = (row) => ({
|
|
65
|
+
at: Number(row.at),
|
|
66
|
+
id: Number(row.id),
|
|
67
|
+
scope: contextCursorScope(request),
|
|
68
|
+
});
|
|
69
|
+
const fit = fittingRows(sized.map((row) => Number(row.bytes)), limit, maxReadBytes);
|
|
70
|
+
const first = sized[0];
|
|
71
|
+
if (!fit.count && first) {
|
|
72
|
+
coverage.hasMore = sized.length > 1;
|
|
73
|
+
return {
|
|
74
|
+
source,
|
|
75
|
+
items: [],
|
|
76
|
+
readBytes: 0,
|
|
77
|
+
coverage,
|
|
78
|
+
next: coverage.hasMore ? past(first) : null,
|
|
79
|
+
oversized: { ref: String(first.ref), bytes: Number(first.bytes) },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
70
82
|
signal?.throwIfAborted();
|
|
71
|
-
|
|
83
|
+
params[params.length - 1] = fit.count;
|
|
84
|
+
const selected = fit.count ? database.prepare(selection).all(...params) : [];
|
|
72
85
|
signal?.throwIfAborted();
|
|
73
|
-
coverage.hasMore =
|
|
74
|
-
const selected = rows.slice(0, limit);
|
|
86
|
+
coverage.hasMore = sized.length > selected.length;
|
|
75
87
|
const last = selected.at(-1);
|
|
76
88
|
return {
|
|
77
89
|
source,
|
|
78
|
-
readBytes,
|
|
90
|
+
readBytes: fit.bytes,
|
|
79
91
|
coverage,
|
|
80
|
-
next: coverage.hasMore && last
|
|
81
|
-
? { at: Number(last.at), id: Number(last.id), scope: contextCursorScope(request) }
|
|
82
|
-
: null,
|
|
92
|
+
next: coverage.hasMore && last ? past(last) : null,
|
|
83
93
|
items: selected.map((row) => ({
|
|
84
94
|
ref: String(row.ref),
|
|
85
95
|
provider: String(row.provider),
|
|
@@ -54,9 +54,9 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
54
54
|
receipt: proposal.result,
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
-
const
|
|
57
|
+
const cite = () => {
|
|
58
58
|
let readBytes = 0;
|
|
59
|
-
return
|
|
59
|
+
return (evidence, _finding, _project, seen) => {
|
|
60
60
|
if (seen.has(evidence.reference)) {
|
|
61
61
|
if (seen.get(evidence.reference) !== evidence.fingerprint)
|
|
62
62
|
return stale();
|
|
@@ -72,8 +72,9 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
72
72
|
if (fingerprint !== evidence.fingerprint)
|
|
73
73
|
return stale();
|
|
74
74
|
seen.set(evidence.reference, fingerprint);
|
|
75
|
-
}
|
|
75
|
+
};
|
|
76
76
|
};
|
|
77
|
+
const snapshot = (input) => core.snapshot(input, cite());
|
|
77
78
|
const decider = (by) => {
|
|
78
79
|
if (by === undefined || by === "owner")
|
|
79
80
|
return { author: "owner", decision: { by: "owner" } };
|
|
@@ -109,6 +110,7 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
109
110
|
return view(proposal);
|
|
110
111
|
}
|
|
111
112
|
const snapshotHash = snapshot(input);
|
|
113
|
+
const snapshotParts = core.snapshotParts(input, cite());
|
|
112
114
|
const fingerprints = input.findings.map(findingFingerprint);
|
|
113
115
|
if (new Set(fingerprints).size !== fingerprints.length)
|
|
114
116
|
return invalid("the batch contains duplicate findings");
|
|
@@ -116,7 +118,7 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
116
118
|
kind: EVIDENCE_FINDINGS_KIND,
|
|
117
119
|
by,
|
|
118
120
|
target: [...projects][0],
|
|
119
|
-
payload: { ...input, inputHash, snapshotHash, agentModel: by.model ?? null },
|
|
121
|
+
payload: { ...input, inputHash, snapshotHash, snapshotParts, agentModel: by.model ?? null },
|
|
120
122
|
}));
|
|
121
123
|
});
|
|
122
124
|
},
|
|
@@ -152,9 +154,21 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
152
154
|
return batch;
|
|
153
155
|
}
|
|
154
156
|
const input = proposal.payload;
|
|
155
|
-
// Rejecting creates nothing, so a batch whose evidence changed can still be closed.
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
// Rejecting creates nothing, so a batch whose evidence changed can still be closed. Accepting needs only the
|
|
158
|
+
// accepted findings' own project, assignee and evidence unchanged; a batch staged before parts were kept needs all.
|
|
159
|
+
if (accepted.length) {
|
|
160
|
+
const parts = input.snapshotParts;
|
|
161
|
+
if (!parts) {
|
|
162
|
+
if (snapshot(batch.input) !== input.snapshotHash)
|
|
163
|
+
return stale();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const chosen = { ...batch.input, findings: batch.input.findings.filter((f) => accepted.includes(f.id)) };
|
|
167
|
+
const now = core.snapshotParts(chosen, cite());
|
|
168
|
+
if (Object.entries(now).some(([key, hash]) => parts[key] !== hash))
|
|
169
|
+
return stale();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
158
172
|
if (proposal.proposedBy.type !== "bot" || !proposal.proposedBy.name)
|
|
159
173
|
return invalid("a findings author must be an agent");
|
|
160
174
|
const agent = {
|
|
@@ -12,6 +12,7 @@ export declare const findingsAgent: (agent: FindingsAgent) => FindingsAgent;
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const findingsCoreOver: (context: StoreContext, kind: string) => {
|
|
14
14
|
snapshot: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string, seen: Map<string, unknown>) => void) => string;
|
|
15
|
+
snapshotParts: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string, seen: Map<string, unknown>) => void) => Record<string, string>;
|
|
15
16
|
previous: (account: number | null, fingerprint: string) => FindingOutcome | undefined;
|
|
16
17
|
outcomes: (account: number | null, input: FindingsInput, accepted: string[], agent: FindingsAgent) => FindingOutcome[];
|
|
17
18
|
};
|
|
@@ -27,7 +27,7 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
27
27
|
const { database } = context;
|
|
28
28
|
const facts = factOperationsOver(context);
|
|
29
29
|
const tasks = workTaskOperationsOver(context);
|
|
30
|
-
const
|
|
30
|
+
const cited = (input, cite) => {
|
|
31
31
|
const seen = new Map();
|
|
32
32
|
for (const finding of input.findings) {
|
|
33
33
|
const project = requiredThing(database, finding.project, "the project");
|
|
@@ -51,8 +51,11 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
51
51
|
for (const evidence of finding.evidence)
|
|
52
52
|
cite(evidence, finding, String(project.id), seen);
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return seen;
|
|
55
55
|
};
|
|
56
|
+
const snapshot = (...args) => findingHash([...cited(...args)].sort(([a], [b]) => a.localeCompare(b)));
|
|
57
|
+
/** The same snapshot, one hash per project, person and cited reference, so part of a batch can be checked alone. */
|
|
58
|
+
const snapshotParts = (...args) => Object.fromEntries([...cited(...args)].map(([key, value]) => [key, findingHash(value)]));
|
|
56
59
|
const previous = (account, fingerprint) => {
|
|
57
60
|
const row = database
|
|
58
61
|
.prepare("SELECT substr(j.value,1,4096) AS item FROM proposed_actions p,json_each(p.result,'$.items') j WHERE p.account_id IS ? AND p.kind=? AND p.status='approved' AND json_extract(j.value,'$.fingerprint')=? ORDER BY p.id LIMIT 1")
|
|
@@ -104,5 +107,5 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
104
107
|
reused: false,
|
|
105
108
|
};
|
|
106
109
|
});
|
|
107
|
-
return { snapshot, previous, outcomes };
|
|
110
|
+
return { snapshot, snapshotParts, previous, outcomes };
|
|
108
111
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.368.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",
|