@wirecat/cli-core 0.551.0 → 0.553.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 +83 -0
- package/README.md +1 -1
- package/dist/background/schedules.d.ts +12 -1
- package/dist/background/schedules.js +65 -24
- package/dist/background/units.d.ts +16 -9
- package/dist/background/units.js +26 -11
- package/dist/cli/compact.d.ts +4 -0
- package/dist/cli/compact.js +65 -7
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/meeting-preparation-answer.d.ts +3 -0
- package/dist/cli/meeting-preparation-projects.d.ts +3 -0
- package/dist/cli/meeting-preparation-read.d.ts +3 -0
- package/dist/cli/messenger/context.d.ts +3 -0
- package/dist/cli/messenger/projects-command.js +38 -11
- package/dist/cli/messenger/search-answers.js +8 -1
- package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
- package/dist/cli/messenger/serve-catch-up.js +90 -0
- package/dist/cli/messenger/serve-command.js +54 -1
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/server-command.d.ts +4 -3
- package/dist/cli/messenger/server-command.js +30 -12
- package/dist/cli/program.js +9 -4
- package/dist/cli/project-evidence.d.ts +33 -4
- package/dist/cli/project-evidence.js +38 -4
- package/dist/cli/project-status-rows.js +11 -1
- package/dist/cli/project-status.d.ts +24 -0
- package/dist/cli/project-status.js +52 -9
- package/dist/cli/schedule/schedule-command.js +5 -1
- package/dist/entities/hit.d.ts +5 -0
- package/dist/entities/hit.js +5 -0
- package/dist/entities/index.d.ts +27 -0
- package/dist/entities/listener.d.ts +6 -0
- package/dist/entities/listener.js +1 -0
- package/dist/entities/meeting-preparation.d.ts +3 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mcp/meeting-preparation.d.ts +3 -0
- package/dist/meetings/answers.d.ts +5 -0
- package/dist/meetings/answers.js +8 -0
- package/dist/meetings/cli/index.js +14 -0
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/ingest.js +2 -1
- package/dist/meetings/language.d.ts +5 -0
- package/dist/meetings/language.js +57 -0
- package/dist/meetings/mcp/search.d.ts +3 -0
- package/dist/meetings/project-evidence-answer.d.ts +3 -0
- package/dist/meetings/result-items.d.ts +9 -0
- package/dist/meetings/result-items.js +7 -0
- package/dist/meetings/store.d.ts +22 -0
- package/dist/meetings/testing/memory.js +3 -0
- package/dist/meetings/transcription.d.ts +2 -0
- package/dist/meetings/transcription.js +5 -1
- package/dist/search/lucene/types.d.ts +7 -0
- package/dist/search/lucene/types.js +11 -1
- package/dist/services/index.d.ts +4 -3
- package/dist/services/index.js +4 -3
- package/dist/services/link-scorers/index.d.ts +1 -1
- package/dist/services/link-scorers/index.js +1 -1
- package/dist/services/link-scorers/scan.d.ts +0 -1
- package/dist/services/link-scorers/scan.js +1 -1
- package/dist/services/messages-archive-search.js +3 -2
- package/dist/services/messages-combined.js +2 -2
- package/dist/services/messages-discovery.js +2 -2
- package/dist/services/notes-search.js +4 -5
- package/dist/services/project-purposes.d.ts +28 -0
- package/dist/services/project-purposes.js +41 -0
- package/dist/services/project-status/index.d.ts +22 -8
- package/dist/services/project-status/index.js +17 -9
- package/dist/services/search-all-meetings.d.ts +3 -0
- package/dist/services/search-all-meetings.js +6 -1
- package/dist/services/stored-project-gathering.d.ts +79 -4
- package/dist/services/stored-project-gathering.js +171 -22
- package/dist/speech/local.d.ts +2 -0
- package/dist/speech/local.js +8 -1
- package/dist/speech/models.d.ts +2 -0
- package/dist/speech/models.js +2 -0
- package/dist/store/driver.d.ts +11 -0
- package/dist/store/driver.js +13 -0
- package/dist/store/drivers/node-sqlite.d.ts +1 -1
- package/dist/store/drivers/node-sqlite.js +3 -0
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.js +2 -2
- package/dist/store/sqlite/lucene.d.ts +1 -1
- package/dist/store/sqlite/lucene.js +33 -7
- package/dist/store/sqlite/meeting-hit-search.js +10 -3
- package/dist/store/sqlite/meeting-languages.d.ts +4 -0
- package/dist/store/sqlite/meeting-languages.js +36 -0
- package/dist/store/sqlite/meetings.d.ts +2 -1
- package/dist/store/sqlite/meetings.js +9 -3
- package/dist/store/sqlite/note-search.js +2 -2
- package/dist/store/sqlite/reads.d.ts +1 -1
- package/dist/store/sqlite/reads.js +2 -2
- package/package.json +1 -1
- package/parity.json +10 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ContextSource } from "../context/evidence.js";
|
|
2
|
-
import type { MessageStore } from "../store/store.js";
|
|
2
|
+
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
3
3
|
import { gatherProjectEvidence, type ProjectChatFetcher, type ProjectGatheringPorts } from "./project-gathering.js";
|
|
4
4
|
import { type EvidenceMentions } from "./project-gathering-mentions.js";
|
|
5
|
+
import { type PurposeConfig } from "./project-purposes.js";
|
|
5
6
|
export interface StoredProjectInput {
|
|
6
7
|
project: string;
|
|
7
8
|
/** The sources the host resolved from its `--source`, and those it could not read. */
|
|
@@ -31,19 +32,81 @@ export interface StoredProjectInput {
|
|
|
31
32
|
maxFetchMs?: number;
|
|
32
33
|
onFetchProgress?: ProjectGatheringPorts["onFetchProgress"];
|
|
33
34
|
signal?: AbortSignal;
|
|
35
|
+
/** The linked chats' purposes to read; `["all"]` reads every one. Left out, the config's defaults. */
|
|
36
|
+
purposes?: readonly string[];
|
|
37
|
+
/** The host's purposes; core's built-in ones when left out. */
|
|
38
|
+
purposeConfig?: PurposeConfig;
|
|
39
|
+
/** Proposed `member-of` links count as linked chats and members too, after the confirmed ones, every item marked. */
|
|
40
|
+
includeProposed?: boolean;
|
|
41
|
+
/** How many messages a left-out chat holds in the window; the store's own count when left out. */
|
|
42
|
+
count?: ChatCounter;
|
|
34
43
|
}
|
|
44
|
+
export interface ChatCount {
|
|
45
|
+
messages: number;
|
|
46
|
+
firstAt: string | null;
|
|
47
|
+
lastAt: string | null;
|
|
48
|
+
}
|
|
49
|
+
export type ChatCounter = (account: AccountKey, chatId: string, window: {
|
|
50
|
+
since: number;
|
|
51
|
+
until: number;
|
|
52
|
+
}) => Promise<ChatCount>;
|
|
53
|
+
/** The store's count of one chat's messages in a window, `until` exclusive, and the first and last of them. */
|
|
54
|
+
export declare const storedChatCount: (store: MessageStore) => ChatCounter;
|
|
35
55
|
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
36
56
|
export declare const canonicalProjectRef: (store: MessageStore, reference: string) => Promise<string>;
|
|
57
|
+
export interface ProjectMember {
|
|
58
|
+
person: string;
|
|
59
|
+
state: "read" | "member-limit" | "no-identity";
|
|
60
|
+
/** The proposal that names this person a member; read only with `includeProposed`. */
|
|
61
|
+
proposed?: string;
|
|
62
|
+
score?: number;
|
|
63
|
+
}
|
|
37
64
|
export interface LinkedChat {
|
|
38
65
|
provider: string;
|
|
39
66
|
account: string;
|
|
40
67
|
chat: string;
|
|
41
68
|
/** The link's free role (`source report`, `what worked`), if the owner set one. */
|
|
42
69
|
role: string | null;
|
|
70
|
+
/** What the chat is for in the project; `null` means `work`. */
|
|
71
|
+
purpose: string | null;
|
|
72
|
+
/** The confirmed link's id; null for a proposed one. */
|
|
73
|
+
relationship: string | null;
|
|
74
|
+
/** The proposal that links the chat; only with `includeProposed`. */
|
|
75
|
+
proposed?: string;
|
|
76
|
+
score?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Links the owner confirmed, then, when asked, the proposed ones: a suggestion would put a whole chat into the
|
|
80
|
+
* evidence on a guess, so it is read only on request and marked.
|
|
81
|
+
*/
|
|
82
|
+
export declare const linkedChatsOf: (store: MessageStore, project: string | null, options?: {
|
|
83
|
+
includeProposed?: boolean;
|
|
84
|
+
}) => Promise<LinkedChat[]>;
|
|
85
|
+
/** A linked chat whose purpose this run did not read: counted, never listed. */
|
|
86
|
+
export interface LeftOutChat {
|
|
87
|
+
ref: string;
|
|
88
|
+
provider: string;
|
|
89
|
+
account: string;
|
|
90
|
+
chat: string;
|
|
91
|
+
title: string | null;
|
|
92
|
+
purpose: string;
|
|
93
|
+
relationship: string | null;
|
|
94
|
+
proposed?: string;
|
|
95
|
+
messages: number;
|
|
96
|
+
firstAt: string | null;
|
|
97
|
+
lastAt: string | null;
|
|
43
98
|
}
|
|
44
|
-
|
|
45
|
-
export
|
|
46
|
-
|
|
99
|
+
type Gathered = Awaited<ReturnType<typeof gatherProjectEvidence>>;
|
|
100
|
+
export type StoredProjectGathering = Omit<Gathered, "items" | "linkedChats"> & {
|
|
101
|
+
/** `proposed`: the proposal whose link alone brought the item in; only with `includeProposed`. */
|
|
102
|
+
items: (Gathered["items"][number] & {
|
|
103
|
+
proposed?: string;
|
|
104
|
+
})[];
|
|
105
|
+
linkedChats: (Gathered["linkedChats"][number] & {
|
|
106
|
+
purpose: string;
|
|
107
|
+
proposed?: string;
|
|
108
|
+
score?: number;
|
|
109
|
+
})[];
|
|
47
110
|
projectRef: string | null;
|
|
48
111
|
unreadSources: {
|
|
49
112
|
source: string;
|
|
@@ -51,9 +114,21 @@ export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvid
|
|
|
51
114
|
reason?: "out-of-scope" | "linked-chat-limit" | "member-limit" | "no-identity" | "unknown-provider";
|
|
52
115
|
}[];
|
|
53
116
|
mentions: EvidenceMentions;
|
|
117
|
+
/** The purposes read: `all`, or the list. */
|
|
118
|
+
purposes: "all" | string[];
|
|
119
|
+
leftOut: LeftOutChat[];
|
|
120
|
+
members: ProjectMember[];
|
|
121
|
+
/** One linked chat with no purpose set gave most of this run's items; a first run only, so it is said once. */
|
|
122
|
+
purposeHint: {
|
|
123
|
+
chat: string;
|
|
124
|
+
relationship: string;
|
|
125
|
+
items: number;
|
|
126
|
+
of: number;
|
|
127
|
+
} | null;
|
|
54
128
|
};
|
|
55
129
|
/**
|
|
56
130
|
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
57
131
|
* confirmed person members and its confirmed linked chats, in the sources the host resolved.
|
|
58
132
|
*/
|
|
59
133
|
export declare const gatherStoredProject: (store: MessageStore, input: StoredProjectInput) => Promise<StoredProjectGathering>;
|
|
134
|
+
export {};
|
|
@@ -4,6 +4,21 @@ import { chatCoverage } from "./chat-coverage.js";
|
|
|
4
4
|
import { gatherProjectEvidence } from "./project-gathering.js";
|
|
5
5
|
import { storedNeighbours } from "./project-gathering-around.js";
|
|
6
6
|
import { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
7
|
+
import { LINK_PURPOSE, purposeRead, purposesOf } from "./project-purposes.js";
|
|
8
|
+
/** The store's count of one chat's messages in a window, `until` exclusive, and the first and last of them. */
|
|
9
|
+
export const storedChatCount = (store) => async (account, chatId, { since, until }) => {
|
|
10
|
+
const window = { since: new Date(since).toISOString(), until: new Date(until - 1).toISOString() };
|
|
11
|
+
const [messages, newest, oldest] = await Promise.all([
|
|
12
|
+
store.messages.countMessages(account, chatId, window),
|
|
13
|
+
store.messages.messages(account, chatId, { limit: 1, since: window.since, until: until - 1 }),
|
|
14
|
+
store.messages.messages(account, chatId, { limit: 1, after: { time: since - 1 }, until: until - 1 }),
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
messages,
|
|
18
|
+
firstAt: oldest.items[0]?.timestamp ?? null,
|
|
19
|
+
lastAt: newest.items.at(-1)?.timestamp ?? null,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
7
22
|
const PROJECT_ID = /^project:[1-9]\d*$/;
|
|
8
23
|
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
9
24
|
export const canonicalProjectRef = async (store, reference) => {
|
|
@@ -85,59 +100,122 @@ const contactOf = async (store, contact) => {
|
|
|
85
100
|
};
|
|
86
101
|
const LINKED_CHATS = 16;
|
|
87
102
|
const CORE_LIMIT = 16;
|
|
88
|
-
/**
|
|
89
|
-
const
|
|
103
|
+
/** Pending `member-of` proposals to the project, oldest first, so a cut at a limit always drops the same ones. */
|
|
104
|
+
const proposedLinksOf = async (store, project) => {
|
|
105
|
+
const stored = await store.proposedActions.list({ kinds: ["relationship"], status: "proposed", project, limit: 500 });
|
|
106
|
+
const proposals = stored
|
|
107
|
+
.flatMap((action) => {
|
|
108
|
+
const payload = action.payload;
|
|
109
|
+
const { purpose, score } = action.payload;
|
|
110
|
+
if (payload?.kind !== "member-of" || payload.to !== project || typeof payload.from !== "string")
|
|
111
|
+
return [];
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
from: payload.from,
|
|
115
|
+
id: action.id,
|
|
116
|
+
role: typeof payload.role === "string" ? payload.role : null,
|
|
117
|
+
purpose: typeof purpose === "string" ? purpose : null,
|
|
118
|
+
...(typeof score === "number" ? { score } : {}),
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
})
|
|
122
|
+
.sort((a, b) => Number(a.id) - Number(b.id));
|
|
123
|
+
const legacy = (await store.knowledge.relations(null, project))
|
|
124
|
+
.filter((one) => !one.confirmed && one.kind === "member-of" && one.to === project)
|
|
125
|
+
.map((one) => ({ from: one.from, id: `link:${one.id}`, role: one.role, purpose: one.purpose }));
|
|
126
|
+
return [...proposals, ...legacy];
|
|
127
|
+
};
|
|
128
|
+
/** Confirmed `person` members in a stable order, then proposed ones when asked, so a cut drops the same ones. */
|
|
129
|
+
const memberRefsOf = async (store, project, includeProposed) => {
|
|
90
130
|
if (!project)
|
|
91
131
|
return [];
|
|
92
|
-
const
|
|
132
|
+
const confirmed = new Set();
|
|
93
133
|
for (const relation of await store.knowledge.relations(null, project))
|
|
94
134
|
if (relation.confirmed &&
|
|
95
135
|
relation.kind === "member-of" &&
|
|
96
136
|
relation.to === project &&
|
|
97
137
|
/^person:.+/.test(relation.from))
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
confirmed.add(relation.from);
|
|
139
|
+
const members = [...confirmed].sort().map((ref) => ({ ref }));
|
|
140
|
+
if (!includeProposed)
|
|
141
|
+
return members;
|
|
142
|
+
for (const one of await proposedLinksOf(store, project))
|
|
143
|
+
if (/^person:.+/.test(one.from) && !members.some((member) => member.ref === one.from))
|
|
144
|
+
members.push({ ref: one.from, proposed: one.id, ...(one.score === undefined ? {} : { score: one.score }) });
|
|
145
|
+
return members;
|
|
100
146
|
};
|
|
101
147
|
/**
|
|
102
148
|
* Members are read by what their identities wrote, like `--contact person:<id>` but without the owner's aliases:
|
|
103
149
|
* a short alias would add a text lane that matches mentions, and membership is about who wrote, not who was named.
|
|
104
150
|
*/
|
|
105
|
-
const memberLanesOf = async (store, project, given, room) => {
|
|
151
|
+
const memberLanesOf = async (store, project, given, room, includeProposed) => {
|
|
106
152
|
const lanes = [];
|
|
107
153
|
const left = [];
|
|
108
|
-
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (const member of (await memberRefsOf(store, project, includeProposed)).filter((one) => !given.has(one.ref))) {
|
|
156
|
+
const { ref: reference, ...marks } = member;
|
|
109
157
|
const person = await store.people.personByUid(reference.slice("person:".length));
|
|
110
158
|
const senders = person ? sendersOf(person) : [];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
else if (lanes.length >= room)
|
|
114
|
-
left.push({ source: reference, kind: "member", reason: "member-limit" });
|
|
115
|
-
else
|
|
159
|
+
const state = !senders.length ? "no-identity" : lanes.length >= room ? "member-limit" : "read";
|
|
160
|
+
if (state === "read")
|
|
116
161
|
lanes.push({ reference, senders });
|
|
162
|
+
else
|
|
163
|
+
left.push({ source: reference, kind: "member", reason: state });
|
|
164
|
+
rows.push({ person: reference, state, ...marks });
|
|
117
165
|
}
|
|
118
|
-
return { lanes, left };
|
|
166
|
+
return { lanes, left, rows };
|
|
119
167
|
};
|
|
120
|
-
|
|
121
|
-
|
|
168
|
+
const chatOf = (from) => {
|
|
169
|
+
if (!from.startsWith("chat:"))
|
|
170
|
+
return undefined;
|
|
171
|
+
const reference = parseReference(from);
|
|
172
|
+
return reference.type === "chat" ? reference : undefined;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Links the owner confirmed, then, when asked, the proposed ones: a suggestion would put a whole chat into the
|
|
176
|
+
* evidence on a guess, so it is read only on request and marked.
|
|
177
|
+
*/
|
|
178
|
+
export const linkedChatsOf = async (store, project, options = {}) => {
|
|
122
179
|
if (!project)
|
|
123
180
|
return [];
|
|
124
181
|
const chats = [];
|
|
125
182
|
for (const relation of await store.knowledge.relations(null, project)) {
|
|
126
183
|
if (!relation.confirmed || relation.kind !== "member-of" || relation.to !== project)
|
|
127
184
|
continue;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const reference = parseReference(relation.from);
|
|
131
|
-
if (reference.type === "chat")
|
|
185
|
+
const reference = chatOf(relation.from);
|
|
186
|
+
if (reference)
|
|
132
187
|
chats.push({
|
|
133
188
|
provider: reference.provider,
|
|
134
189
|
account: reference.account,
|
|
135
190
|
chat: reference.chat,
|
|
136
191
|
role: relation.role,
|
|
192
|
+
purpose: relation.purpose,
|
|
193
|
+
relationship: relation.id,
|
|
137
194
|
});
|
|
138
195
|
}
|
|
196
|
+
if (!options.includeProposed)
|
|
197
|
+
return chats;
|
|
198
|
+
for (const one of await proposedLinksOf(store, project)) {
|
|
199
|
+
const reference = chatOf(one.from);
|
|
200
|
+
if (!reference)
|
|
201
|
+
continue;
|
|
202
|
+
if (chats.some((chat) => chat.provider === reference.provider && chat.account === reference.account && chat.chat === reference.chat))
|
|
203
|
+
continue;
|
|
204
|
+
chats.push({
|
|
205
|
+
provider: reference.provider,
|
|
206
|
+
account: reference.account,
|
|
207
|
+
chat: reference.chat,
|
|
208
|
+
role: one.role,
|
|
209
|
+
purpose: one.purpose,
|
|
210
|
+
relationship: null,
|
|
211
|
+
proposed: one.id,
|
|
212
|
+
...(one.score === undefined ? {} : { score: one.score }),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
139
215
|
return chats;
|
|
140
216
|
};
|
|
217
|
+
/** Over half the items and at least this many, from one chat's membership alone: worth asking what the chat is for. */
|
|
218
|
+
const HINT_ITEMS = 20;
|
|
141
219
|
/**
|
|
142
220
|
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
143
221
|
* confirmed person members and its confirmed linked chats, in the sources the host resolved.
|
|
@@ -160,9 +238,12 @@ export const gatherStoredProject = async (store, input) => {
|
|
|
160
238
|
(input.keywords?.length ?? 0) +
|
|
161
239
|
contacts.filter((one) => one.name).length;
|
|
162
240
|
const room = Math.min(CORE_LIMIT - contacts.length, CORE_LIMIT - searchLanes - contacts.filter((one) => one.senders).length);
|
|
163
|
-
const
|
|
241
|
+
const includeProposed = input.includeProposed === true;
|
|
242
|
+
const purposes = purposesOf(input.purposes, input.purposeConfig, project);
|
|
243
|
+
const members = await memberLanesOf(store, project.ref, new Set(contacts.flatMap((one) => one.reference ?? [])), room, includeProposed);
|
|
164
244
|
const people = [...contacts, ...members.lanes];
|
|
165
|
-
const
|
|
245
|
+
const all = await linkedChatsOf(store, project.ref, { includeProposed });
|
|
246
|
+
const linked = all.filter((one) => purposeRead(purposes, one.purpose));
|
|
166
247
|
const linkedChats = linked.slice(0, LINKED_CHATS).map(({ provider, account, chat }) => ({ provider, account, chat }));
|
|
167
248
|
const budgets = {
|
|
168
249
|
...(input.maxReads === undefined ? {} : { maxReads: input.maxReads }),
|
|
@@ -208,5 +289,73 @@ export const gatherStoredProject = async (store, input) => {
|
|
|
208
289
|
reason: "linked-chat-limit",
|
|
209
290
|
})),
|
|
210
291
|
];
|
|
211
|
-
|
|
292
|
+
const refOf = (one) => formatReference({ type: "chat", ...one });
|
|
293
|
+
const byRef = new Map(linked.map((one) => [refOf(one), one]));
|
|
294
|
+
const proposedLanes = new Map();
|
|
295
|
+
for (const row of gathered.linkedChats) {
|
|
296
|
+
const proposed = byRef.get(row.ref)?.proposed;
|
|
297
|
+
if (proposed && row.lane !== null)
|
|
298
|
+
proposedLanes.set(row.lane, proposed);
|
|
299
|
+
}
|
|
300
|
+
for (const lane of gathered.lanes) {
|
|
301
|
+
const proposed = members.rows.find((one) => one.proposed && lane.people.includes(one.person))?.proposed;
|
|
302
|
+
if (proposed && lane.kinds.includes("sender"))
|
|
303
|
+
proposedLanes.set(lane.id, proposed);
|
|
304
|
+
}
|
|
305
|
+
// Marked only when nothing but proposed links found it: an item a word or a confirmed link found is evidence anyway.
|
|
306
|
+
const items = gathered.items.map((item) => {
|
|
307
|
+
const proposed = item.lanes.every((lane) => proposedLanes.has(lane))
|
|
308
|
+
? proposedLanes.get(item.lanes[0] ?? "")
|
|
309
|
+
: undefined;
|
|
310
|
+
return proposed ? { ...item, proposed } : item;
|
|
311
|
+
});
|
|
312
|
+
const linkedRows = gathered.linkedChats.map((row) => {
|
|
313
|
+
const link = byRef.get(row.ref);
|
|
314
|
+
return {
|
|
315
|
+
...row,
|
|
316
|
+
purpose: link?.purpose ?? LINK_PURPOSE,
|
|
317
|
+
...(link?.proposed ? { proposed: link.proposed } : {}),
|
|
318
|
+
...(link?.score === undefined ? {} : { score: link.score }),
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
const count = input.count ?? storedChatCount(store);
|
|
322
|
+
const leftOut = [];
|
|
323
|
+
for (const one of all.filter((chat) => !purposeRead(purposes, chat.purpose))) {
|
|
324
|
+
const account = { provider: one.provider, account: one.account };
|
|
325
|
+
const [counted, [held]] = await Promise.all([
|
|
326
|
+
count(account, one.chat, { since: gathered.window.after, until: gathered.window.before }),
|
|
327
|
+
chatCoverage(store, account, [one.chat]),
|
|
328
|
+
]);
|
|
329
|
+
leftOut.push({
|
|
330
|
+
ref: refOf(one),
|
|
331
|
+
provider: one.provider,
|
|
332
|
+
account: one.account,
|
|
333
|
+
chat: one.chat,
|
|
334
|
+
title: held?.title ?? null,
|
|
335
|
+
purpose: one.purpose ?? LINK_PURPOSE,
|
|
336
|
+
relationship: one.relationship,
|
|
337
|
+
...(one.proposed ? { proposed: one.proposed } : {}),
|
|
338
|
+
...counted,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
projectRef: project.ref,
|
|
343
|
+
unreadSources: [...unread, ...unreadChats, ...members.left],
|
|
344
|
+
...gathered,
|
|
345
|
+
items,
|
|
346
|
+
linkedChats: linkedRows,
|
|
347
|
+
mentions,
|
|
348
|
+
purposes: purposes === "all" ? "all" : [...purposes],
|
|
349
|
+
leftOut,
|
|
350
|
+
members: members.rows,
|
|
351
|
+
purposeHint: gathered.resumed ? null : purposeHintOf(linked, linkedRows, items.length),
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
const purposeHintOf = (linked, rows, of) => {
|
|
355
|
+
const [top] = [...rows].sort((a, b) => (b.items ?? 0) - (a.items ?? 0));
|
|
356
|
+
const items = top?.items ?? 0;
|
|
357
|
+
const link = linked.find((one) => formatReference({ type: "chat", ...one }) === top?.ref);
|
|
358
|
+
if (!top || !link?.relationship || link.purpose !== null || items < HINT_ITEMS || items * 2 <= of)
|
|
359
|
+
return null;
|
|
360
|
+
return { chat: top.ref, relationship: link.relationship, items, of };
|
|
212
361
|
};
|
package/dist/speech/local.d.ts
CHANGED
package/dist/speech/local.js
CHANGED
|
@@ -23,7 +23,14 @@ export const localSpeechRecognitionModel = (id) => {
|
|
|
23
23
|
featureDim: model.featureDim,
|
|
24
24
|
}))
|
|
25
25
|
.digest("hex");
|
|
26
|
-
return Object.freeze({
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
engine: "sherpa-onnx",
|
|
28
|
+
model: model.id,
|
|
29
|
+
version,
|
|
30
|
+
...(model.language === undefined ? {} : { language: model.language }),
|
|
31
|
+
sampleRate: 16000,
|
|
32
|
+
maxChunkSamples: 480000,
|
|
33
|
+
});
|
|
27
34
|
};
|
|
28
35
|
const notInstalled = (id) => new CliError("not_found", `speech model ${id} is not installed; download it explicitly with a WireCat messenger's models download command`, { reason: "model.not-downloaded", model: id });
|
|
29
36
|
/**
|
package/dist/speech/models.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface SpeechModel {
|
|
|
18
18
|
id: string;
|
|
19
19
|
title: string;
|
|
20
20
|
languages: string;
|
|
21
|
+
/** The one language the model writes, so its transcripts need no guessing; absent for multilingual models. */
|
|
22
|
+
language?: string;
|
|
21
23
|
files: ModelFile[];
|
|
22
24
|
/** The part of sherpa-onnx's `modelConfig` that names the files, given where they are. */
|
|
23
25
|
config: (path: (name: string) => string) => Record<string, unknown>;
|
package/dist/speech/models.js
CHANGED
|
@@ -8,6 +8,7 @@ export const MODELS = [
|
|
|
8
8
|
id: "gigaam-v3",
|
|
9
9
|
title: "GigaAM v3, with punctuation",
|
|
10
10
|
languages: "Russian — the best of the three for Russian",
|
|
11
|
+
language: "ru",
|
|
11
12
|
featureDim: 64,
|
|
12
13
|
files: [
|
|
13
14
|
{
|
|
@@ -45,6 +46,7 @@ export const MODELS = [
|
|
|
45
46
|
id: "gigaam-v3-ctc",
|
|
46
47
|
title: "GigaAM v3 CTC, with punctuation — a little faster",
|
|
47
48
|
languages: "Russian — a little faster, rougher with capital letters",
|
|
49
|
+
language: "ru",
|
|
48
50
|
featureDim: 64,
|
|
49
51
|
files: [
|
|
50
52
|
{
|
package/dist/store/driver.d.ts
CHANGED
|
@@ -23,7 +23,18 @@ export interface CacheDatabase {
|
|
|
23
23
|
exec(sql: string): void;
|
|
24
24
|
prepare(sql: string): CacheStatement;
|
|
25
25
|
close(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Set when the connection has `TIME_LIMIT_FUNCTION`. A statement runs to its end before any timer can fire, so a
|
|
28
|
+
* long one is stopped from inside: `WHERE … AND wc_time_limit(<epoch ms>)` throws `TimeLimitReached` once past it.
|
|
29
|
+
*/
|
|
30
|
+
timeLimit?: true;
|
|
26
31
|
}
|
|
32
|
+
export declare const TIME_LIMIT_FUNCTION = "wc_time_limit";
|
|
33
|
+
export declare class TimeLimitReached extends Error {
|
|
34
|
+
constructor();
|
|
35
|
+
}
|
|
36
|
+
/** The SQL function's body: 1 until `until` (epoch milliseconds) has passed. */
|
|
37
|
+
export declare const beforeTimeLimit: (until: unknown) => number;
|
|
27
38
|
export type OpenDatabase = (path: string) => CacheDatabase;
|
|
28
39
|
/**
|
|
29
40
|
* **Write-ahead mode, and a timeout rather than an instant failure.**
|
package/dist/store/driver.js
CHANGED
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
* The surface is deliberately tiny. Everything above it is ordinary SQL, so replacing the driver —
|
|
10
10
|
* or adding a third — is writing thirty lines, not learning an abstraction.
|
|
11
11
|
*/
|
|
12
|
+
export const TIME_LIMIT_FUNCTION = "wc_time_limit";
|
|
13
|
+
export class TimeLimitReached extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("the statement ran past its time limit");
|
|
16
|
+
this.name = "TimeLimitReached";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The SQL function's body: 1 until `until` (epoch milliseconds) has passed. */
|
|
20
|
+
export const beforeTimeLimit = (until) => {
|
|
21
|
+
if (Date.now() > Number(until))
|
|
22
|
+
throw new TimeLimitReached();
|
|
23
|
+
return 1;
|
|
24
|
+
};
|
|
12
25
|
/**
|
|
13
26
|
* **Write-ahead mode, and a timeout rather than an instant failure.**
|
|
14
27
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import type
|
|
2
|
+
import { type CacheDatabase } from "../driver.js";
|
|
3
3
|
/**
|
|
4
4
|
* Import this module only through a dynamic import that follows `quietSqliteWarning()`: a static import loads
|
|
5
5
|
* `node:sqlite`, and on Node 22 its ExperimentalWarning, before the filter can run.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all lint/plugin/no-sqlite-driver: cli-core owns the store, so it opens the driver itself
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { beforeTimeLimit, TIME_LIMIT_FUNCTION, } from "../driver.js";
|
|
3
4
|
import { loadVectorExtension, vectorExtension } from "../vector-extension.js";
|
|
4
5
|
/**
|
|
5
6
|
* Import this module only through a dynamic import that follows `quietSqliteWarning()`: a static import loads
|
|
@@ -29,6 +30,7 @@ export const nodeClient = async (path, { readOnly = false } = {}) => {
|
|
|
29
30
|
export const STATEMENT_CACHE_SIZE = 1000;
|
|
30
31
|
export const cacheOverNodeSqlite = (database, cacheSize = STATEMENT_CACHE_SIZE) => {
|
|
31
32
|
const statements = new Map();
|
|
33
|
+
database.function(TIME_LIMIT_FUNCTION, { deterministic: false, directOnly: true }, beforeTimeLimit);
|
|
32
34
|
const prepare = (sql) => {
|
|
33
35
|
const statement = database.prepare(sql);
|
|
34
36
|
return {
|
|
@@ -38,6 +40,7 @@ export const cacheOverNodeSqlite = (database, cacheSize = STATEMENT_CACHE_SIZE)
|
|
|
38
40
|
};
|
|
39
41
|
};
|
|
40
42
|
return {
|
|
43
|
+
timeLimit: true,
|
|
41
44
|
exec: (sql) => database.exec(sql),
|
|
42
45
|
prepare: (sql) => {
|
|
43
46
|
const cached = statements.get(sql);
|
package/dist/store/messages.d.ts
CHANGED
|
@@ -45,9 +45,10 @@ export interface AccountMessages {
|
|
|
45
45
|
* ends the window; `reads.MessagePageWindow` says how.
|
|
46
46
|
*/
|
|
47
47
|
messages(key: AccountKey, chatId: Id, window: reads.MessagePageWindow): Promise<Page<Message>>;
|
|
48
|
-
/** How many stored messages the chat has, sent at `since` or later
|
|
48
|
+
/** How many stored messages the chat has, sent at `since` or later and at `until` or earlier when given. */
|
|
49
49
|
countMessages(key: AccountKey, chatId: Id, options?: {
|
|
50
50
|
since?: string;
|
|
51
|
+
until?: string;
|
|
51
52
|
}): Promise<number>;
|
|
52
53
|
/** Saved or edited after `at` (ISO, this machine's clock), oldest first, and the ids deleted after it. */
|
|
53
54
|
changes(key: AccountKey, chatId: Id, at: string): Promise<{
|
package/dist/store/messages.js
CHANGED
|
@@ -32,9 +32,9 @@ export const accountMessagesOver = (context) => {
|
|
|
32
32
|
const chatKey = chatKeyOf(key, chatId);
|
|
33
33
|
return chatKey === undefined ? { items: [], hasMore: false } : reads.messagePage(context, chatKey, window);
|
|
34
34
|
},
|
|
35
|
-
countMessages: async (key, chatId, { since } = {}) => {
|
|
35
|
+
countMessages: async (key, chatId, { since, until } = {}) => {
|
|
36
36
|
const chatKey = chatKeyOf(key, chatId);
|
|
37
|
-
return chatKey === undefined ? 0 : reads.countMessages(context, chatKey, since);
|
|
37
|
+
return chatKey === undefined ? 0 : reads.countMessages(context, chatKey, since, until);
|
|
38
38
|
},
|
|
39
39
|
changes: async (key, chatId, at) => {
|
|
40
40
|
const chatKey = chatKeyOf(key, chatId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Page } from "../../domain/models.js";
|
|
2
2
|
import type { QueryExecution } from "../../search/lucene/resolved.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type SqlValue } from "../driver.js";
|
|
4
4
|
import type { StoreContext } from "./open.js";
|
|
5
5
|
import type { ScoredHit } from "./words.js";
|
|
6
6
|
export { type Corpus, prefixOf } from "./lucene-fields.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { compileAutomaton } from "../../search/lucene/automaton.js";
|
|
3
3
|
import { isStemmed } from "../../search/lucene/resolved.js";
|
|
4
|
-
import { exhausted, QUERY_LIMITS, searchError } from "../../search/lucene/types.js";
|
|
4
|
+
import { exhausted, outOfTime, QUERY_LIMITS, searchError } from "../../search/lucene/types.js";
|
|
5
|
+
import { TIME_LIMIT_FUNCTION, TimeLimitReached } from "../driver.js";
|
|
5
6
|
import { emailHitsByPk } from "./emails.js";
|
|
6
7
|
import { placeHits } from "./hit-places.js";
|
|
7
8
|
import { combine, FIELD_COMPILERS, ftsMatch, SPEECH, } from "./lucene-fields.js";
|
|
@@ -54,7 +55,7 @@ export const countMatches = (context, execution, cap) => matchingPks(context, ex
|
|
|
54
55
|
* The keys of the messages that match, no more than `cap + 1`: one SQL query, unranked. `undefined` when a leaf is
|
|
55
56
|
* checked row by row (a preset, a pattern), where it would cost as much as the search itself.
|
|
56
57
|
*/
|
|
57
|
-
export const matchingPks = (context, execution, cap) => {
|
|
58
|
+
export const matchingPks = (context, execution, cap) => inTime(() => {
|
|
58
59
|
const { database, check, where, from, withRanking, ranked, leaves } = compileQuery(context, {
|
|
59
60
|
...execution,
|
|
60
61
|
newest: true,
|
|
@@ -66,6 +67,19 @@ export const matchingPks = (context, execution, cap) => {
|
|
|
66
67
|
.all(...ranked.params, ...where.params, cap + 1);
|
|
67
68
|
check();
|
|
68
69
|
return rows.map(({ id }) => Number(id));
|
|
70
|
+
});
|
|
71
|
+
const timeLimited = (error) => {
|
|
72
|
+
if (error instanceof TimeLimitReached)
|
|
73
|
+
outOfTime();
|
|
74
|
+
throw error;
|
|
75
|
+
};
|
|
76
|
+
const inTime = (work) => {
|
|
77
|
+
try {
|
|
78
|
+
return work();
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return timeLimited(error);
|
|
82
|
+
}
|
|
69
83
|
};
|
|
70
84
|
const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
71
85
|
const { database } = context;
|
|
@@ -74,6 +88,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
74
88
|
if (mail && (execution.conversationIds || execution.conversationSince !== undefined))
|
|
75
89
|
throw new CliError("validation_error", "mail has no conversations to search within");
|
|
76
90
|
const started = context.now();
|
|
91
|
+
const until = Date.now() + QUERY_LIMITS.milliseconds;
|
|
77
92
|
const budget = { work: 0 };
|
|
78
93
|
let states = 0, expansions = 0;
|
|
79
94
|
const patterns = new Map();
|
|
@@ -82,7 +97,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
82
97
|
if (execution.signal?.aborted)
|
|
83
98
|
throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
|
|
84
99
|
if (context.now() - started > QUERY_LIMITS.milliseconds)
|
|
85
|
-
|
|
100
|
+
outOfTime();
|
|
86
101
|
};
|
|
87
102
|
const patternOf = (pattern, node) => {
|
|
88
103
|
check();
|
|
@@ -197,7 +212,10 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
197
212
|
exact: true,
|
|
198
213
|
})), "OR"),
|
|
199
214
|
], "AND");
|
|
200
|
-
|
|
215
|
+
const timeLimit = database.timeLimit
|
|
216
|
+
? [{ sql: `${TIME_LIMIT_FUNCTION}(?)`, params: [until], exact: true }]
|
|
217
|
+
: [];
|
|
218
|
+
let where = combine([{ sql: "m.deleted_at IS NULL", params: [], exact: true }, scope, expression, ...timeLimit], "AND");
|
|
201
219
|
if (execution.conversationIds !== undefined || execution.conversationSince !== undefined) {
|
|
202
220
|
const ids = execution.conversationIds;
|
|
203
221
|
where = combine([
|
|
@@ -404,7 +422,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
404
422
|
/** The matcher and its consumer run synchronously in one read snapshot; exact keys stay in SQL. */
|
|
405
423
|
export const withQuerySelection = (context, execution, consume) => {
|
|
406
424
|
const { database } = context;
|
|
407
|
-
return read(database, () => {
|
|
425
|
+
return inTime(() => read(database, () => {
|
|
408
426
|
const query = compileQuery(context, execution, true);
|
|
409
427
|
const { check, leaves, where, from, withRanking, ranked } = query;
|
|
410
428
|
check();
|
|
@@ -448,9 +466,17 @@ export const withQuerySelection = (context, execution, consume) => {
|
|
|
448
466
|
throw new Error("query selection consumers must run synchronously");
|
|
449
467
|
check();
|
|
450
468
|
return result;
|
|
451
|
-
});
|
|
469
|
+
}));
|
|
470
|
+
};
|
|
471
|
+
const runQuery = (context, execution, by) => {
|
|
472
|
+
try {
|
|
473
|
+
return runCompiled(context, execution, by).catch(timeLimited);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
return timeLimited(error);
|
|
477
|
+
}
|
|
452
478
|
};
|
|
453
|
-
const
|
|
479
|
+
const runCompiled = async (context, execution, by) => {
|
|
454
480
|
const { corpus, rowsFrom, hits, database, check, grouped, where, from, withRanking, ranked, leaves, stemmed, exactTier, driver, baseFrom, filtered, ranking, relevance, rankedFrom, exactColumn, order, placeTier, projection, projectionParams, joinedFrom, evaluate, } = compileQuery(context, execution);
|
|
455
481
|
if (by === "pks" && leaves.every(({ test }) => !test)) {
|
|
456
482
|
const rows = database
|
|
@@ -121,9 +121,9 @@ export function meetingHitSearchOver({ database }) {
|
|
|
121
121
|
if (after)
|
|
122
122
|
params.push(after.tier, after.tier, after.started, after.started, after.meeting, after.meeting, after.kind, after.kind, after.id);
|
|
123
123
|
const sql = `WITH matches AS (${matching}), matching AS (SELECT rid,tier FROM matches h WHERE NOT EXISTS(SELECT 1 FROM meeting_index_pending p WHERE p.id=h.rid/4 AND p.indexable_type=CASE h.rid%4 WHEN 1 THEN 'meeting_transcript_row' WHEN 2 THEN 'meeting_chat_message' ELSE 'meeting_summary' END)), selected AS (SELECT m.id,coalesce(m.started_at,0) AS started FROM meetings m WHERE ${clauses.join(" AND ")}), hits AS (
|
|
124
|
-
SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
|
|
125
|
-
UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
|
|
126
|
-
UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
|
|
124
|
+
SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position,t.language,(SELECT count(*) FROM meeting_transcript_rows n WHERE n.meeting_transcript_id=t.id) AS lines_total FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
|
|
125
|
+
UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL,NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
|
|
126
|
+
UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL,NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
|
|
127
127
|
SELECT h.*,${bytes("m", ["title"])}+${bytes("s", ["doc_url"])} AS context_bytes FROM hits h
|
|
128
128
|
JOIN meetings m ON m.id=h.meeting_id LEFT JOIN meeting_summaries s ON h.kind=2 AND s.id=h.id
|
|
129
129
|
${seek} ORDER BY h.tier,h.started DESC,h.meeting_id,h.kind,h.id LIMIT ?`;
|
|
@@ -176,6 +176,13 @@ export function meetingHitSearchOver({ database }) {
|
|
|
176
176
|
id: Number(row.id),
|
|
177
177
|
text,
|
|
178
178
|
startMs: row.start_ms === null ? null : Number(row.start_ms),
|
|
179
|
+
...(kind === 0
|
|
180
|
+
? {
|
|
181
|
+
language: row.language === null ? null : String(row.language),
|
|
182
|
+
line: Number(row.position) + 1,
|
|
183
|
+
linesTotal: Number(row.lines_total),
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
179
186
|
};
|
|
180
187
|
});
|
|
181
188
|
const fresh = items.flatMap((item) => {
|