@wirecat/cli-core 0.365.0 → 0.367.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 +32 -0
- 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/meeting-communications-preparation.js +4 -0
- package/dist/services/message-resolution.js +19 -6
- 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/store/chats.d.ts +7 -1
- package/dist/store/chats.js +13 -5
- package/dist/store/sqlite/chat-rekey.d.ts +2 -1
- package/dist/store/sqlite/chat-rekey.js +23 -4
- package/dist/store/sqlite/chats.d.ts +13 -0
- package/dist/store/sqlite/chats.js +37 -5
- 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/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +16 -0
- package/dist/store/sqlite/schema.d.ts +195 -0
- package/dist/store/sqlite/schema.js +25 -0
- package/dist/store/sync.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.367.0 — 20.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A document hit in project gathering carries the paragraphs around it.** `around` holds, in document order, up
|
|
15
|
+
to `around.before` and `around.after` paragraphs next to the first one naming a searched word, plus that one
|
|
16
|
+
marked `found`. The bytes count as `usage.aroundBytes`, as a message's neighbours do.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **An evidence findings review accepts the findings whose own evidence is unchanged.** Before, one changed text
|
|
21
|
+
anywhere in a batch refused accepting any of it, so the agent had to stage the whole batch again. Now only the
|
|
22
|
+
accepted findings' project, assignee and cited texts are rechecked; the rest can be rejected in the same review.
|
|
23
|
+
Batches keep one hash per checked record for this; a batch staged earlier is still checked as a whole.
|
|
24
|
+
|
|
25
|
+
- **Project gathering moves past large documents instead of stalling.** A page of evidence now holds the records
|
|
26
|
+
that fit its byte budget together, rather than failing when they do not. A record larger than the page is read
|
|
27
|
+
alone, up to 256 KiB. One larger still is listed in the new `oversized` result field by reference and skipped,
|
|
28
|
+
so a resume no longer retries the same page and charges its budget each time. The context reader's page gains
|
|
29
|
+
the same optional `oversized`. Meeting preparation reports such a record as `read-limited` and continues past it.
|
|
30
|
+
|
|
31
|
+
## 0.366.0 — 20.10.2026
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **A chat's history in the store (store version 25).** `chat_revisions` keeps one row per change to a chat's id,
|
|
36
|
+
title, membership, scope, search visibility or parent, with the old and new value, the time and the store write
|
|
37
|
+
that made it (`chats`, `delta`, `left`, `rejoin`, `rekey`, `merge`, `send`, `details`; NULL for an older build or SQL
|
|
38
|
+
by hand). A trigger writes it in the changing transaction, only for a real change. A read by the id a chat was
|
|
39
|
+
re-keyed from, `messages list <old id> --offline` included, now finds the chat it became, per account
|
|
40
|
+
(`store.chats.renamedChatId`). The history is never indexed for search; a purged chat's history goes with it.
|
|
41
|
+
|
|
10
42
|
## 0.365.0 — 19.10.2026
|
|
11
43
|
|
|
12
44
|
### Fixed
|
|
@@ -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)
|
|
@@ -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();
|
|
@@ -23,35 +23,48 @@ export const readChatId = async (deps, reference, store, account) => {
|
|
|
23
23
|
};
|
|
24
24
|
const LEFT_EMPTY = "you left this chat, and the store holds none of its messages";
|
|
25
25
|
/** A chat as typed, found among the stored chats the way an adapter finds it among its own. */
|
|
26
|
-
export const storedChatId = async (messenger, reference, store, account) => chatIdAmong(messenger, reference, account,
|
|
26
|
+
export const storedChatId = async (messenger, reference, store, account) => chatIdAmong(messenger, reference, account, {
|
|
27
|
+
held: () => store.chats.chats(account, {}),
|
|
28
|
+
renamed: (id) => store.chats.renamedChatId(account, id),
|
|
29
|
+
});
|
|
27
30
|
/** `storedChatId`, then among the chats the account left: their history is kept, so a read of it still finds them. */
|
|
28
31
|
const keptChatId = (messenger, reference, store, account) => storedChatId(messenger, reference, store, account).catch((error) => {
|
|
29
32
|
if (codeOf(error) !== "not_found")
|
|
30
33
|
throw error;
|
|
31
|
-
return chatIdAmong(messenger, reference, account,
|
|
34
|
+
return chatIdAmong(messenger, reference, account, {
|
|
35
|
+
held: () => store.chats.chats(account, { includeLeft: true }),
|
|
36
|
+
renamed: (id) => store.chats.renamedChatId(account, id),
|
|
37
|
+
});
|
|
32
38
|
});
|
|
33
39
|
/** `storedChatId`, among the chats of the account the store is bound to; `includeLeft` tries the left ones last. */
|
|
34
40
|
export const chatIdIn = (messenger, reference, store, { includeLeft = false } = {}) => {
|
|
35
|
-
const
|
|
41
|
+
const renamed = (id) => store.chats.renamedChatId(id);
|
|
42
|
+
const among = chatIdAmong(messenger, reference, store.account, { held: () => store.chats.chats({}), renamed });
|
|
36
43
|
if (!includeLeft)
|
|
37
44
|
return among;
|
|
38
45
|
return among.catch((error) => {
|
|
39
46
|
if (codeOf(error) !== "not_found")
|
|
40
47
|
throw error;
|
|
41
|
-
return chatIdAmong(messenger, reference, store.account,
|
|
48
|
+
return chatIdAmong(messenger, reference, store.account, {
|
|
49
|
+
held: () => store.chats.chats({ includeLeft: true }),
|
|
50
|
+
renamed,
|
|
51
|
+
});
|
|
42
52
|
});
|
|
43
53
|
};
|
|
44
|
-
const chatIdAmong = async (messenger, reference, account, held) => {
|
|
54
|
+
const chatIdAmong = async (messenger, reference, account, { held, renamed }) => {
|
|
45
55
|
const trimmed = reference.trim();
|
|
46
56
|
if (messenger.savedChatId && ["me", "self", "saved"].includes(trimmed.toLowerCase())) {
|
|
47
57
|
return messenger.savedChatId(account);
|
|
48
58
|
}
|
|
49
59
|
if (/^-?\d+$/.test(trimmed))
|
|
50
|
-
return trimmed;
|
|
60
|
+
return (await renamed(trimmed)) ?? trimmed;
|
|
51
61
|
const chats = (await held()).items;
|
|
52
62
|
const exact = chats.find((one) => one.id === trimmed);
|
|
53
63
|
if (exact)
|
|
54
64
|
return exact.id;
|
|
65
|
+
const moved = await renamed(trimmed);
|
|
66
|
+
if (moved !== undefined && chats.some((one) => one.id === moved))
|
|
67
|
+
return moved;
|
|
55
68
|
if (trimmed.startsWith("@")) {
|
|
56
69
|
const username = trimmed.slice(1).toLowerCase();
|
|
57
70
|
const found = chats.find((one) => String(one.providerMetadata?.username ?? "").toLowerCase() === username);
|
|
@@ -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,
|
package/dist/store/chats.d.ts
CHANGED
|
@@ -36,7 +36,8 @@ export interface ChatsStore {
|
|
|
36
36
|
rejoinChats(key: AccountKey, present: Id[]): Promise<number>;
|
|
37
37
|
/**
|
|
38
38
|
* The chat the messenger now names `to` instead of `from`: renamed, or merged into the chat already stored as
|
|
39
|
-
* `to
|
|
39
|
+
* `to`, with `from` recorded in `chat_revisions` (`renamedChatId`), also when only `to` is stored. Replaying it
|
|
40
|
+
* changes nothing.
|
|
40
41
|
*/
|
|
41
42
|
rekeyChat(key: AccountKey, from: Id, to: Id): Promise<ChatRekey>;
|
|
42
43
|
/**
|
|
@@ -58,6 +59,11 @@ export interface ChatsStore {
|
|
|
58
59
|
chatCompleteness(key: AccountKey, chatIds: Id[]): Promise<ChatCompleteness[]>;
|
|
59
60
|
/** Per chat that has messages: how many, the oldest and newest, and when the last one was stored. */
|
|
60
61
|
chatStats(key: AccountKey, chatId?: Id): Promise<ChatStats[]>;
|
|
62
|
+
/**
|
|
63
|
+
* The id a chat re-keyed from `chatId` has now, from `chat_revisions`; `undefined` while a chat is stored under
|
|
64
|
+
* `chatId` itself. Only for finding the chat: the old id and its history are never shown or searched.
|
|
65
|
+
*/
|
|
66
|
+
renamedChatId(key: AccountKey, chatId: Id): Promise<Id | undefined>;
|
|
61
67
|
/** A stored chat's kind, with or without messages; `undefined` when the store does not know the chat. */
|
|
62
68
|
chatKind(key: AccountKey, chatId: Id): Promise<ChatKind | undefined>;
|
|
63
69
|
/** Per chat that spoke since `since` (ms): its newest message and who sent it when stored, never the text. */
|
package/dist/store/chats.js
CHANGED
|
@@ -11,8 +11,10 @@ export const chatsStoreOver = (context) => {
|
|
|
11
11
|
return {
|
|
12
12
|
saveChats: async (key, list) => write(database, () => {
|
|
13
13
|
const accountKey = accountPk(key);
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
chatQueries.revisedBy(context, "chats", () => {
|
|
15
|
+
for (const chat of list)
|
|
16
|
+
chatQueries.upsertChat(context, accountKey, chat);
|
|
17
|
+
});
|
|
16
18
|
}),
|
|
17
19
|
chats: async (key, window) => {
|
|
18
20
|
const accountKey = findAccountPk(key);
|
|
@@ -34,14 +36,16 @@ export const chatsStoreOver = (context) => {
|
|
|
34
36
|
return 0;
|
|
35
37
|
let marked = 0;
|
|
36
38
|
write(database, () => {
|
|
37
|
-
marked = chatQueries.markLeft(context, accountKey, present);
|
|
39
|
+
marked = chatQueries.revisedBy(context, "left", () => chatQueries.markLeft(context, accountKey, present));
|
|
38
40
|
writeState(accountKey, completeness.CHAT_LIST_KEY, String(present.length));
|
|
39
41
|
});
|
|
40
42
|
return marked;
|
|
41
43
|
},
|
|
42
44
|
rejoinChats: async (key, present) => {
|
|
43
45
|
const accountKey = findAccountPk(key);
|
|
44
|
-
return accountKey === undefined
|
|
46
|
+
return accountKey === undefined
|
|
47
|
+
? 0
|
|
48
|
+
: write(database, () => chatQueries.revisedBy(context, "rejoin", () => chatQueries.rejoin(context, accountKey, present)));
|
|
45
49
|
},
|
|
46
50
|
rekeyChat: async (key, from, to) => {
|
|
47
51
|
const accountKey = findAccountPk(key);
|
|
@@ -77,6 +81,10 @@ export const chatsStoreOver = (context) => {
|
|
|
77
81
|
const accountKey = findAccountPk(key);
|
|
78
82
|
return accountKey === undefined ? [] : reads.chatStats(context, accountKey, chatId);
|
|
79
83
|
},
|
|
84
|
+
renamedChatId: async (key, chatId) => {
|
|
85
|
+
const accountKey = findAccountPk(key);
|
|
86
|
+
return accountKey === undefined ? undefined : chatQueries.renamedChatId(context, accountKey, chatId);
|
|
87
|
+
},
|
|
80
88
|
chatKind: async (key, chatId) => {
|
|
81
89
|
const accountKey = findAccountPk(key);
|
|
82
90
|
if (accountKey === undefined)
|
|
@@ -93,7 +101,7 @@ export const chatsStoreOver = (context) => {
|
|
|
93
101
|
: reads.chatActivity(context, accountKey, since, where);
|
|
94
102
|
},
|
|
95
103
|
chatMetadata: async (key, chatId) => chatQueries.metadata(context, key, chatId),
|
|
96
|
-
saveChatMetadata: async (key, entry) => chatQueries.saveMetadata(context, key, entry),
|
|
104
|
+
saveChatMetadata: async (key, entry) => write(database, () => chatQueries.saveMetadata(context, key, entry)),
|
|
97
105
|
replaceAutoTags: async (key, chatId, algorithm, matches) => write(database, () => chatQueries.replaceAutoTags(context, key, chatId, algorithm, matches)),
|
|
98
106
|
rankingDiscussionChats: async (account, chatId) => {
|
|
99
107
|
const rows = database
|
|
@@ -24,7 +24,8 @@ export interface ChatRekeys {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* A chat the messenger now names by another id: renamed in place, or merged into the row already stored under the
|
|
27
|
-
* new id's twin
|
|
27
|
+
* new id's twin, each change recorded in `chat_revisions`. Replaying it changes nothing; naming a chat the store does
|
|
28
|
+
* not hold records only the old id, when the new one is stored. Runs inside `write`.
|
|
28
29
|
*/
|
|
29
30
|
export declare const rekeyChat: (context: StoreContext, key: AccountKey, accountKey: number, from: Id, to: Id) => ChatRekey;
|
|
30
31
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { formatLocator } from "../../domain/locator.js";
|
|
3
|
-
import { purgeChats, stricterPrivacy } from "./chats.js";
|
|
3
|
+
import { purgeChats, revisedBy, stricterPrivacy } from "./chats.js";
|
|
4
4
|
import { pointerPairs } from "./entity-types.js";
|
|
5
5
|
import { tombstone } from "./messages.js";
|
|
6
6
|
import { targetKey } from "./notes.js";
|
|
@@ -113,6 +113,7 @@ const absorb = (context, kept, gone) => {
|
|
|
113
113
|
foldOpenStays(context, kept, gone);
|
|
114
114
|
for (const table of [
|
|
115
115
|
"poll_votes",
|
|
116
|
+
"chat_revisions",
|
|
116
117
|
"chat_events",
|
|
117
118
|
"message_transcripts",
|
|
118
119
|
"chat_members",
|
|
@@ -157,15 +158,33 @@ const MESSENGER_FIELDS = [
|
|
|
157
158
|
"description",
|
|
158
159
|
"details_fetched_at",
|
|
159
160
|
];
|
|
161
|
+
/**
|
|
162
|
+
* The re-key of a chat stored only under its new id changes no row, so the trigger records nothing; the old id is
|
|
163
|
+
* written by hand, once, so a read by it still finds the chat.
|
|
164
|
+
*/
|
|
165
|
+
const recordOldId = ({ database, now }, accountKey, chat, from, to) => {
|
|
166
|
+
database
|
|
167
|
+
.prepare(`INSERT INTO chat_revisions (chat_id, account_id, field, old_value, new_value, source, created_at)
|
|
168
|
+
SELECT ?, ?, 'external_id', ?, ?, 'rekey', ? WHERE NOT EXISTS (SELECT 1 FROM chat_revisions
|
|
169
|
+
WHERE chat_id = ? AND field = 'external_id' AND old_value = ?)`)
|
|
170
|
+
.run(chat, accountKey, from, to, now(), chat, from);
|
|
171
|
+
};
|
|
160
172
|
/**
|
|
161
173
|
* A chat the messenger now names by another id: renamed in place, or merged into the row already stored under the
|
|
162
|
-
* new id's twin
|
|
174
|
+
* new id's twin, each change recorded in `chat_revisions`. Replaying it changes nothing; naming a chat the store does
|
|
175
|
+
* not hold records only the old id, when the new one is stored. Runs inside `write`.
|
|
163
176
|
*/
|
|
164
177
|
export const rekeyChat = (context, key, accountKey, from, to) => {
|
|
165
178
|
const old = from === to ? undefined : chatRow(context, accountKey, from);
|
|
166
|
-
|
|
179
|
+
const twin = from === to ? undefined : chatRow(context, accountKey, to);
|
|
180
|
+
if (!old) {
|
|
181
|
+
if (twin)
|
|
182
|
+
recordOldId(context, accountKey, Number(twin.id), from, to);
|
|
167
183
|
return { outcome: "unchanged", messages: 0, duplicates: 0 };
|
|
168
|
-
|
|
184
|
+
}
|
|
185
|
+
return revisedBy(context, twin ? "merge" : "rekey", () => moveChat(context, key, accountKey, old, twin, from, to));
|
|
186
|
+
};
|
|
187
|
+
const moveChat = (context, key, accountKey, old, twin, from, to) => {
|
|
169
188
|
const kept = Number(old.id);
|
|
170
189
|
let moved = { messages: 0, duplicates: 0 };
|
|
171
190
|
if (twin) {
|
|
@@ -7,6 +7,19 @@ export declare const chatOf: ({ orm }: StoreContext, accountKey: number, chatKey
|
|
|
7
7
|
export declare const findChatPk: ({ orm }: StoreContext, accountKey: number, chatId: Id) => number | undefined;
|
|
8
8
|
/** A chat known only from its messages, until the chat list names it. */
|
|
9
9
|
export declare const chatPkFor: (context: StoreContext, accountKey: number, chatId: Id) => number;
|
|
10
|
+
/** The store write a `chat_revisions` row names; a writer that sets none is recorded as NULL. */
|
|
11
|
+
export type ChatRevisionSource = "chats" | "delta" | "left" | "rejoin" | "rekey" | "merge" | "send" | "details";
|
|
12
|
+
/**
|
|
13
|
+
* Runs `body` with `source` as what the `chat_revisions` trigger records, inside the caller's transaction. The row is
|
|
14
|
+
* gone again before the commit, so an older build's write in a later transaction records no source rather than this
|
|
15
|
+
* one; a nested call puts the outer source back.
|
|
16
|
+
*/
|
|
17
|
+
export declare const revisedBy: <T>({ database, now }: StoreContext, source: ChatRevisionSource, body: () => T) => T;
|
|
18
|
+
/**
|
|
19
|
+
* The id the chat once stored as `chatId` has now, from its newest re-key in this account; `undefined` while a chat is
|
|
20
|
+
* stored under `chatId` itself, or none ever was.
|
|
21
|
+
*/
|
|
22
|
+
export declare const renamedChatId: ({ database }: StoreContext, accountKey: number, chatId: Id) => Id | undefined;
|
|
10
23
|
interface ChatPrivacy {
|
|
11
24
|
searchable: number;
|
|
12
25
|
scope: string | null;
|
|
@@ -29,6 +29,38 @@ export const chatPkFor = (context, accountKey, chatId) => findChatPk(context, ac
|
|
|
29
29
|
})
|
|
30
30
|
.returning({ pk: chats.id })
|
|
31
31
|
.get()?.pk);
|
|
32
|
+
/**
|
|
33
|
+
* Runs `body` with `source` as what the `chat_revisions` trigger records, inside the caller's transaction. The row is
|
|
34
|
+
* gone again before the commit, so an older build's write in a later transaction records no source rather than this
|
|
35
|
+
* one; a nested call puts the outer source back.
|
|
36
|
+
*/
|
|
37
|
+
export const revisedBy = ({ database, now }, source, body) => {
|
|
38
|
+
const outer = database.prepare("SELECT source, at FROM chat_revision_source WHERE id = 1").get();
|
|
39
|
+
const set = database.prepare("INSERT OR REPLACE INTO chat_revision_source (id, source, at) VALUES (1, ?, ?)");
|
|
40
|
+
set.run(source, now());
|
|
41
|
+
try {
|
|
42
|
+
return body();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (outer)
|
|
46
|
+
set.run(String(outer.source), Number(outer.at));
|
|
47
|
+
else
|
|
48
|
+
database.prepare("DELETE FROM chat_revision_source").run();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The id the chat once stored as `chatId` has now, from its newest re-key in this account; `undefined` while a chat is
|
|
53
|
+
* stored under `chatId` itself, or none ever was.
|
|
54
|
+
*/
|
|
55
|
+
export const renamedChatId = ({ database }, accountKey, chatId) => {
|
|
56
|
+
const row = database
|
|
57
|
+
.prepare(`SELECT c.external_id AS id FROM chat_revisions r JOIN chats c ON c.id = r.chat_id
|
|
58
|
+
WHERE r.account_id = ? AND r.field = 'external_id' AND r.old_value = ? AND c.account_id = r.account_id
|
|
59
|
+
AND NOT EXISTS (SELECT 1 FROM chats WHERE account_id = ? AND external_id = ?)
|
|
60
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT 1`)
|
|
61
|
+
.get(accountKey, chatId, accountKey, chatId);
|
|
62
|
+
return row ? String(row.id) : undefined;
|
|
63
|
+
};
|
|
32
64
|
/**
|
|
33
65
|
* The owner's word on a chat must not loosen because another row joins it: hidden wins, and when the two chats sit
|
|
34
66
|
* on different sides of the owner's life, the personal side wins, since a work chat reaches work views and meeting
|
|
@@ -70,11 +102,11 @@ export const answeredChatPk = (context, accountKey, chatId, typed) => {
|
|
|
70
102
|
if (found !== undefined) {
|
|
71
103
|
const strict = stricterPrivacy(context, accountKey, found, like);
|
|
72
104
|
if (strict.searchable !== found.searchable || strict.scope !== found.scope)
|
|
73
|
-
orm
|
|
105
|
+
revisedBy(context, "send", () => orm
|
|
74
106
|
.update(chats)
|
|
75
107
|
.set({ ...strict, updatedAt: now() })
|
|
76
108
|
.where(eq(chats.id, found.pk))
|
|
77
|
-
.run();
|
|
109
|
+
.run());
|
|
78
110
|
return found.pk;
|
|
79
111
|
}
|
|
80
112
|
return Number(orm
|
|
@@ -185,7 +217,7 @@ export const rejoin = ({ orm }, accountKey, present) => orm
|
|
|
185
217
|
* list. Answers how many it marked.
|
|
186
218
|
*/
|
|
187
219
|
export const markLeft = (context, accountKey, present) => {
|
|
188
|
-
rejoin(context, accountKey, present);
|
|
220
|
+
revisedBy(context, "rejoin", () => rejoin(context, accountKey, present));
|
|
189
221
|
return context.orm
|
|
190
222
|
.update(chats)
|
|
191
223
|
.set({ membershipState: "left" })
|
|
@@ -352,9 +384,9 @@ export const metadata = (context, key, chatId) => {
|
|
|
352
384
|
: undefined;
|
|
353
385
|
};
|
|
354
386
|
export const saveMetadata = (context, key, entry) => {
|
|
355
|
-
context.database
|
|
387
|
+
revisedBy(context, "details", () => context.database
|
|
356
388
|
.prepare("UPDATE chats SET title=?, username=?, description=?, details_fetched_at=?, updated_at=? WHERE id=?")
|
|
357
|
-
.run(entry.title, entry.username, entry.description, context.now(), context.now(), metadataChatPk(context, key, entry.chatId));
|
|
389
|
+
.run(entry.title, entry.username, entry.description, context.now(), context.now(), metadataChatPk(context, key, entry.chatId)));
|
|
358
390
|
return metadata(context, key, entry.chatId);
|
|
359
391
|
};
|
|
360
392
|
/** Called inside the store's transaction: claims and effective search tags change together. */
|
|
@@ -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
|
};
|
|
@@ -29,6 +29,8 @@ export const MANIFEST = [
|
|
|
29
29
|
{ name: "20261018024850_version-23-chat-events", version: 23, minCompatible: 1 },
|
|
30
30
|
{ name: "20261018061512_version-24-message-stars", version: 24, minCompatible: 1 },
|
|
31
31
|
{ name: "20261018063952_version-24-message-stars-index", version: 24, minCompatible: 1 },
|
|
32
|
+
{ name: "20261019115025_version-25-chat-revisions", version: 25, minCompatible: 1 },
|
|
33
|
+
{ name: "20261019115027_version-25-chat-revisions-triggers", version: 25, minCompatible: 1 },
|
|
32
34
|
];
|
|
33
35
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
34
36
|
const migrations = [];
|
|
@@ -538,5 +538,21 @@ export const GENERATED = [
|
|
|
538
538
|
"statements": [
|
|
539
539
|
"CREATE INDEX `messages_starred_by_time` ON `messages` (`chat_id`,\"sent_at\" desc) WHERE starred = 1;"
|
|
540
540
|
]
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"name": "20261019115025_version-25-chat-revisions",
|
|
544
|
+
"statements": [
|
|
545
|
+
"CREATE TABLE `chat_revision_source` (\n\t`id` integer PRIMARY KEY,\n\t`source` text NOT NULL,\n\t`at` integer NOT NULL\n);",
|
|
546
|
+
"CREATE TABLE `chat_revisions` (\n\t`id` integer PRIMARY KEY,\n\t`chat_id` integer NOT NULL,\n\t`account_id` integer NOT NULL,\n\t`field` text NOT NULL,\n\t`old_value` text,\n\t`new_value` text,\n\t`source` text,\n\t`created_at` integer NOT NULL\n);",
|
|
547
|
+
"CREATE INDEX `chat_revisions_by_chat_id_created_at` ON `chat_revisions` (`chat_id`,`created_at`);",
|
|
548
|
+
"CREATE INDEX `chat_revisions_by_account_id_old_value` ON `chat_revisions` (`account_id`,`old_value`) WHERE field = 'external_id';"
|
|
549
|
+
]
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"name": "20261019115027_version-25-chat-revisions-triggers",
|
|
553
|
+
"statements": [
|
|
554
|
+
"CREATE TRIGGER chat_revisions_au AFTER UPDATE OF external_id, title, membership_state, scope, searchable, parent_chat_id ON chats BEGIN\n INSERT INTO chat_revisions (chat_id, account_id, field, old_value, new_value, source, created_at)\n SELECT new.id, new.account_id, f.field, f.old_value, f.new_value, s.source,\n coalesce(s.at, CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER))\n FROM (\n SELECT 'external_id' AS field, old.external_id AS old_value, new.external_id AS new_value\n UNION ALL SELECT 'title', old.title, new.title\n UNION ALL SELECT 'membership_state', old.membership_state, new.membership_state\n UNION ALL SELECT 'scope', old.scope, new.scope\n UNION ALL SELECT 'searchable', old.searchable, new.searchable\n UNION ALL SELECT 'parent_chat_id', old.parent_chat_id, new.parent_chat_id\n ) f\n LEFT JOIN chat_revision_source s ON s.id = 1\n WHERE f.old_value IS NOT f.new_value;\nEND;",
|
|
555
|
+
"CREATE TRIGGER chat_revisions_ad AFTER DELETE ON chats BEGIN\n DELETE FROM chat_revisions WHERE chat_id = old.id;\nEND;"
|
|
556
|
+
]
|
|
541
557
|
}
|
|
542
558
|
];
|
|
@@ -4092,6 +4092,201 @@ export declare const pollVotes: import("drizzle-orm/sqlite-core").SQLiteTableWit
|
|
|
4092
4092
|
};
|
|
4093
4093
|
dialect: 'sqlite';
|
|
4094
4094
|
}>;
|
|
4095
|
+
/**
|
|
4096
|
+
* One row per field a chat changed, written by a trigger on `chats` in the changing statement's transaction. No foreign
|
|
4097
|
+
* keys: an older build purges chats without knowing this table, and a trigger drops a purged chat's rows instead.
|
|
4098
|
+
*/
|
|
4099
|
+
export declare const chatRevisions: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
4100
|
+
name: "chat_revisions";
|
|
4101
|
+
schema: undefined;
|
|
4102
|
+
columns: {
|
|
4103
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4104
|
+
name: string;
|
|
4105
|
+
tableName: "chat_revisions";
|
|
4106
|
+
dataType: "number int53";
|
|
4107
|
+
data: number;
|
|
4108
|
+
driverParam: number;
|
|
4109
|
+
notNull: true;
|
|
4110
|
+
hasDefault: true;
|
|
4111
|
+
isPrimaryKey: true;
|
|
4112
|
+
isAutoincrement: false;
|
|
4113
|
+
hasRuntimeDefault: false;
|
|
4114
|
+
enumValues: undefined;
|
|
4115
|
+
baseColumn: never;
|
|
4116
|
+
identity: undefined;
|
|
4117
|
+
generated: undefined;
|
|
4118
|
+
}, {}>;
|
|
4119
|
+
chatId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4120
|
+
name: string;
|
|
4121
|
+
tableName: "chat_revisions";
|
|
4122
|
+
dataType: "number int53";
|
|
4123
|
+
data: number;
|
|
4124
|
+
driverParam: number;
|
|
4125
|
+
notNull: true;
|
|
4126
|
+
hasDefault: false;
|
|
4127
|
+
isPrimaryKey: false;
|
|
4128
|
+
isAutoincrement: false;
|
|
4129
|
+
hasRuntimeDefault: false;
|
|
4130
|
+
enumValues: undefined;
|
|
4131
|
+
baseColumn: never;
|
|
4132
|
+
identity: undefined;
|
|
4133
|
+
generated: undefined;
|
|
4134
|
+
}, {}>;
|
|
4135
|
+
accountId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4136
|
+
name: string;
|
|
4137
|
+
tableName: "chat_revisions";
|
|
4138
|
+
dataType: "number int53";
|
|
4139
|
+
data: number;
|
|
4140
|
+
driverParam: number;
|
|
4141
|
+
notNull: true;
|
|
4142
|
+
hasDefault: false;
|
|
4143
|
+
isPrimaryKey: false;
|
|
4144
|
+
isAutoincrement: false;
|
|
4145
|
+
hasRuntimeDefault: false;
|
|
4146
|
+
enumValues: undefined;
|
|
4147
|
+
baseColumn: never;
|
|
4148
|
+
identity: undefined;
|
|
4149
|
+
generated: undefined;
|
|
4150
|
+
}, {}>;
|
|
4151
|
+
field: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4152
|
+
name: string;
|
|
4153
|
+
tableName: "chat_revisions";
|
|
4154
|
+
dataType: "string";
|
|
4155
|
+
data: string;
|
|
4156
|
+
driverParam: string;
|
|
4157
|
+
notNull: true;
|
|
4158
|
+
hasDefault: false;
|
|
4159
|
+
isPrimaryKey: false;
|
|
4160
|
+
isAutoincrement: false;
|
|
4161
|
+
hasRuntimeDefault: false;
|
|
4162
|
+
enumValues: [string, ...string[]];
|
|
4163
|
+
baseColumn: never;
|
|
4164
|
+
identity: undefined;
|
|
4165
|
+
generated: undefined;
|
|
4166
|
+
}, {}>;
|
|
4167
|
+
oldValue: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4168
|
+
name: string;
|
|
4169
|
+
tableName: "chat_revisions";
|
|
4170
|
+
dataType: "string";
|
|
4171
|
+
data: string;
|
|
4172
|
+
driverParam: string;
|
|
4173
|
+
notNull: false;
|
|
4174
|
+
hasDefault: false;
|
|
4175
|
+
isPrimaryKey: false;
|
|
4176
|
+
isAutoincrement: false;
|
|
4177
|
+
hasRuntimeDefault: false;
|
|
4178
|
+
enumValues: [string, ...string[]];
|
|
4179
|
+
baseColumn: never;
|
|
4180
|
+
identity: undefined;
|
|
4181
|
+
generated: undefined;
|
|
4182
|
+
}, {}>;
|
|
4183
|
+
newValue: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4184
|
+
name: string;
|
|
4185
|
+
tableName: "chat_revisions";
|
|
4186
|
+
dataType: "string";
|
|
4187
|
+
data: string;
|
|
4188
|
+
driverParam: string;
|
|
4189
|
+
notNull: false;
|
|
4190
|
+
hasDefault: false;
|
|
4191
|
+
isPrimaryKey: false;
|
|
4192
|
+
isAutoincrement: false;
|
|
4193
|
+
hasRuntimeDefault: false;
|
|
4194
|
+
enumValues: [string, ...string[]];
|
|
4195
|
+
baseColumn: never;
|
|
4196
|
+
identity: undefined;
|
|
4197
|
+
generated: undefined;
|
|
4198
|
+
}, {}>;
|
|
4199
|
+
source: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4200
|
+
name: string;
|
|
4201
|
+
tableName: "chat_revisions";
|
|
4202
|
+
dataType: "string";
|
|
4203
|
+
data: string;
|
|
4204
|
+
driverParam: string;
|
|
4205
|
+
notNull: false;
|
|
4206
|
+
hasDefault: false;
|
|
4207
|
+
isPrimaryKey: false;
|
|
4208
|
+
isAutoincrement: false;
|
|
4209
|
+
hasRuntimeDefault: false;
|
|
4210
|
+
enumValues: [string, ...string[]];
|
|
4211
|
+
baseColumn: never;
|
|
4212
|
+
identity: undefined;
|
|
4213
|
+
generated: undefined;
|
|
4214
|
+
}, {}>;
|
|
4215
|
+
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4216
|
+
name: string;
|
|
4217
|
+
tableName: "chat_revisions";
|
|
4218
|
+
dataType: "number int53";
|
|
4219
|
+
data: number;
|
|
4220
|
+
driverParam: number;
|
|
4221
|
+
notNull: true;
|
|
4222
|
+
hasDefault: false;
|
|
4223
|
+
isPrimaryKey: false;
|
|
4224
|
+
isAutoincrement: false;
|
|
4225
|
+
hasRuntimeDefault: false;
|
|
4226
|
+
enumValues: undefined;
|
|
4227
|
+
baseColumn: never;
|
|
4228
|
+
identity: undefined;
|
|
4229
|
+
generated: undefined;
|
|
4230
|
+
}, {}>;
|
|
4231
|
+
};
|
|
4232
|
+
dialect: 'sqlite';
|
|
4233
|
+
}>;
|
|
4234
|
+
/** The source and time the `chat_revisions` trigger records, set by the store inside its write and gone by commit. */
|
|
4235
|
+
export declare const chatRevisionSource: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
4236
|
+
name: "chat_revision_source";
|
|
4237
|
+
schema: undefined;
|
|
4238
|
+
columns: {
|
|
4239
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4240
|
+
name: string;
|
|
4241
|
+
tableName: "chat_revision_source";
|
|
4242
|
+
dataType: "number int53";
|
|
4243
|
+
data: number;
|
|
4244
|
+
driverParam: number;
|
|
4245
|
+
notNull: true;
|
|
4246
|
+
hasDefault: true;
|
|
4247
|
+
isPrimaryKey: true;
|
|
4248
|
+
isAutoincrement: false;
|
|
4249
|
+
hasRuntimeDefault: false;
|
|
4250
|
+
enumValues: undefined;
|
|
4251
|
+
baseColumn: never;
|
|
4252
|
+
identity: undefined;
|
|
4253
|
+
generated: undefined;
|
|
4254
|
+
}, {}>;
|
|
4255
|
+
source: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4256
|
+
name: string;
|
|
4257
|
+
tableName: "chat_revision_source";
|
|
4258
|
+
dataType: "string";
|
|
4259
|
+
data: string;
|
|
4260
|
+
driverParam: string;
|
|
4261
|
+
notNull: true;
|
|
4262
|
+
hasDefault: false;
|
|
4263
|
+
isPrimaryKey: false;
|
|
4264
|
+
isAutoincrement: false;
|
|
4265
|
+
hasRuntimeDefault: false;
|
|
4266
|
+
enumValues: [string, ...string[]];
|
|
4267
|
+
baseColumn: never;
|
|
4268
|
+
identity: undefined;
|
|
4269
|
+
generated: undefined;
|
|
4270
|
+
}, {}>;
|
|
4271
|
+
at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
4272
|
+
name: string;
|
|
4273
|
+
tableName: "chat_revision_source";
|
|
4274
|
+
dataType: "number int53";
|
|
4275
|
+
data: number;
|
|
4276
|
+
driverParam: number;
|
|
4277
|
+
notNull: true;
|
|
4278
|
+
hasDefault: false;
|
|
4279
|
+
isPrimaryKey: false;
|
|
4280
|
+
isAutoincrement: false;
|
|
4281
|
+
hasRuntimeDefault: false;
|
|
4282
|
+
enumValues: undefined;
|
|
4283
|
+
baseColumn: never;
|
|
4284
|
+
identity: undefined;
|
|
4285
|
+
generated: undefined;
|
|
4286
|
+
}, {}>;
|
|
4287
|
+
};
|
|
4288
|
+
dialect: 'sqlite';
|
|
4289
|
+
}>;
|
|
4095
4290
|
/** Changes to who is in a chat, and to its title, for a messenger whose history is pushed to the store. */
|
|
4096
4291
|
export declare const chatEvents: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
4097
4292
|
name: "chat_events";
|
|
@@ -457,6 +457,31 @@ export const pollVotes = sqliteTable("poll_votes", {
|
|
|
457
457
|
unique().on(table.chatId, table.messageExternalId, table.identityId),
|
|
458
458
|
index("poll_votes_by_identity_id").on(table.identityId),
|
|
459
459
|
]);
|
|
460
|
+
/**
|
|
461
|
+
* One row per field a chat changed, written by a trigger on `chats` in the changing statement's transaction. No foreign
|
|
462
|
+
* keys: an older build purges chats without knowing this table, and a trigger drops a purged chat's rows instead.
|
|
463
|
+
*/
|
|
464
|
+
export const chatRevisions = sqliteTable("chat_revisions", {
|
|
465
|
+
id: integer("id").primaryKey(),
|
|
466
|
+
chatId: integer("chat_id").notNull(),
|
|
467
|
+
accountId: integer("account_id").notNull(),
|
|
468
|
+
field: text("field").notNull(),
|
|
469
|
+
oldValue: text("old_value"),
|
|
470
|
+
newValue: text("new_value"),
|
|
471
|
+
source: text("source"),
|
|
472
|
+
createdAt: integer("created_at").notNull(),
|
|
473
|
+
}, (table) => [
|
|
474
|
+
index("chat_revisions_by_chat_id_created_at").on(table.chatId, table.createdAt),
|
|
475
|
+
index("chat_revisions_by_account_id_old_value")
|
|
476
|
+
.on(table.accountId, table.oldValue)
|
|
477
|
+
.where(sql `field = 'external_id'`),
|
|
478
|
+
]);
|
|
479
|
+
/** The source and time the `chat_revisions` trigger records, set by the store inside its write and gone by commit. */
|
|
480
|
+
export const chatRevisionSource = sqliteTable("chat_revision_source", {
|
|
481
|
+
id: integer("id").primaryKey(),
|
|
482
|
+
source: text("source").notNull(),
|
|
483
|
+
at: integer("at").notNull(),
|
|
484
|
+
});
|
|
460
485
|
/** Changes to who is in a chat, and to its title, for a messenger whose history is pushed to the store. */
|
|
461
486
|
export const chatEvents = sqliteTable("chat_events", {
|
|
462
487
|
id: integer("id").primaryKey(),
|
package/dist/store/sync.js
CHANGED
|
@@ -20,8 +20,10 @@ export const syncStoreOver = (context) => {
|
|
|
20
20
|
},
|
|
21
21
|
applyDelta: async (key, { chats = [], people = [], members = new Map(), state = {} }) => write(database, () => {
|
|
22
22
|
const accountKey = accountPk(key);
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
chatQueries.revisedBy(context, "delta", () => {
|
|
24
|
+
for (const chat of chats)
|
|
25
|
+
chatQueries.upsertChat(context, accountKey, chat);
|
|
26
|
+
});
|
|
25
27
|
for (const person of people)
|
|
26
28
|
identityPk(accountKey, key.provider, person.id, person.name, person);
|
|
27
29
|
for (const [chatId, ids] of members)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.367.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",
|