@wirecat/cli-core 0.563.0 → 0.564.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/messenger/archive-commands.js +17 -0
  4. package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
  5. package/dist/cli/messenger/catch-up-answers.js +3 -0
  6. package/dist/cli/messenger/search-answers.d.ts +1 -0
  7. package/dist/cli/messenger/search-answers.js +1 -1
  8. package/dist/cli/messenger/stats-answers.d.ts +3 -3
  9. package/dist/cli/messenger/stats-answers.js +21 -9
  10. package/dist/cli/messenger/tasks-command.js +35 -14
  11. package/dist/conversations/mail-text.d.ts +6 -0
  12. package/dist/conversations/mail-text.js +73 -0
  13. package/dist/domain/calendar.d.ts +2 -0
  14. package/dist/domain/calendar.js +6 -0
  15. package/dist/entities/chat-stats.d.ts +16 -2
  16. package/dist/entities/chat-stats.js +18 -3
  17. package/dist/entities/index.d.ts +50 -802
  18. package/dist/entities/index.js +23 -16
  19. package/dist/entities/mail-counts.d.ts +15 -0
  20. package/dist/entities/mail-counts.js +16 -0
  21. package/dist/entities/meeting.d.ts +1 -1
  22. package/dist/entities/meeting.js +1 -1
  23. package/dist/entities/store-records.d.ts +8 -0
  24. package/dist/entities/store-records.js +8 -0
  25. package/dist/errors/reasons.js +5 -0
  26. package/dist/mail/cli/index.d.ts +5 -0
  27. package/dist/mail/cli/index.js +27 -3
  28. package/dist/mail/cli/review.js +11 -4
  29. package/dist/mail/index.d.ts +2 -0
  30. package/dist/mail/index.js +2 -0
  31. package/dist/mail/stats.d.ts +50 -0
  32. package/dist/mail/stats.js +60 -0
  33. package/dist/mail/whole.d.ts +4 -4
  34. package/dist/mail/whole.js +4 -3
  35. package/dist/meetings/index.d.ts +2 -2
  36. package/dist/meetings/index.js +2 -2
  37. package/dist/meetings/mcp/index.d.ts +15 -15
  38. package/dist/meetings/read-answers.d.ts +22 -1
  39. package/dist/meetings/read-answers.js +38 -0
  40. package/dist/meetings/stats.d.ts +44 -0
  41. package/dist/meetings/stats.js +77 -7
  42. package/dist/search/lucene/registry.js +2 -1
  43. package/dist/search/question-plan.d.ts +16 -0
  44. package/dist/search/question-plan.js +34 -0
  45. package/dist/services/events-search.d.ts +5 -1
  46. package/dist/services/events-search.js +10 -4
  47. package/dist/services/index.d.ts +1 -1
  48. package/dist/services/index.js +1 -1
  49. package/dist/services/meeting-stats.d.ts +12 -1
  50. package/dist/services/meeting-stats.js +12 -0
  51. package/dist/services/messages-discovery.d.ts +1 -5
  52. package/dist/services/messages-discovery.js +5 -18
  53. package/dist/services/messages-meaning.js +25 -3
  54. package/dist/services/messages.js +4 -3
  55. package/dist/services/notes-search.d.ts +5 -1
  56. package/dist/services/notes-search.js +16 -5
  57. package/dist/services/proposals/ids.d.ts +2 -2
  58. package/dist/services/proposals/ids.js +5 -3
  59. package/dist/services/proposals/kinds/findings.d.ts +1 -1
  60. package/dist/services/proposals/kinds/findings.js +1 -1
  61. package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
  62. package/dist/services/proposals/kinds/meeting-findings.js +73 -0
  63. package/dist/services/proposals/kinds/task-close.d.ts +7 -0
  64. package/dist/services/proposals/kinds/task-close.js +76 -0
  65. package/dist/services/proposals/registry.js +4 -0
  66. package/dist/services/search-all.d.ts +5 -0
  67. package/dist/services/search-all.js +47 -14
  68. package/dist/services/task-stats.d.ts +41 -0
  69. package/dist/services/task-stats.js +126 -0
  70. package/dist/services/tasks.d.ts +17 -4
  71. package/dist/services/tasks.js +31 -14
  72. package/dist/store/chats.d.ts +3 -1
  73. package/dist/store/chats.js +6 -0
  74. package/dist/store/decisions.d.ts +4 -2
  75. package/dist/store/decisions.js +5 -3
  76. package/dist/store/exports.d.ts +1 -1
  77. package/dist/store/exports.js +1 -1
  78. package/dist/store/index.d.ts +2 -1
  79. package/dist/store/index.js +1 -1
  80. package/dist/store/sqlite/completeness.d.ts +17 -0
  81. package/dist/store/sqlite/completeness.js +49 -13
  82. package/dist/store/sqlite/lucene-fields.js +2 -0
  83. package/dist/store/sqlite/lucene-groups.d.ts +15 -0
  84. package/dist/store/sqlite/lucene-groups.js +21 -0
  85. package/dist/store/sqlite/lucene.d.ts +2 -9
  86. package/dist/store/sqlite/lucene.js +1 -12
  87. package/dist/store/sqlite/meeting-stats.d.ts +2 -0
  88. package/dist/store/sqlite/meeting-stats.js +27 -1
  89. package/dist/store/sqlite/retention.js +2 -7
  90. package/dist/store/sqlite/task-board.js +2 -0
  91. package/dist/store/sqlite/task-stats.d.ts +32 -0
  92. package/dist/store/sqlite/task-stats.js +45 -0
  93. package/dist/store/sqlite/tasks.d.ts +2 -1
  94. package/dist/store/sqlite/tasks.js +2 -0
  95. package/package.json +1 -1
  96. package/parity.json +13 -1
@@ -70,11 +70,7 @@ within?: () => Promise<number[] | undefined>) => Promise<{
70
70
  export declare const RANK_WITHIN_MESSAGES = 100000;
71
71
  /** What a hit not found by the query's words must match: as a search request's text or AST. */
72
72
  type Scope = Pick<SearchQuery, "text" | "ast">;
73
- /**
74
- * The query discovery searches. A question's own "?" would parse as a one-character wildcard and send it to strict
75
- * search (BUG-395); the colon of a time or ratio ("22:00", "1:1") would parse as a field and fail the whole search,
76
- * and escaped it is syntax again, so it splits the numbers (BUG-450). A quoted timestamp keeps its colons (BUG-458).
77
- */
73
+ /** The query discovery searches: a question's text cleaned as `questionText` says. */
78
74
  export declare const discoveryRequest: (asked: SearchQuery) => SearchQuery;
79
75
  export declare const searchDiscovery: (store: MessageStore, account: AccountKey, asked: SearchQuery, messenger?: Partial<Pick<MessengerTraits, "savedChatId" | "app" | "history" | "provider">>, meaning?: MeaningLane, rerank?: RerankLane) => Promise<SearchFound>;
80
76
  /** The nearest messages the question's filters allow, as hits, nearest first. */
@@ -4,7 +4,7 @@ import { parseLocator } from "../domain/locator.js";
4
4
  import { fieldsOfQuery, meaningWords } from "../search/lucene/meaning-parts.js";
5
5
  import { validateFields } from "../search/lucene/registry.js";
6
6
  import { exhausted, MESSAGE_PARSING, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
7
- import { planQuestion, words } from "../search/question-plan.js";
7
+ import { anyOfTerms, lookupTerms, planQuestion, questionText, words } from "../search/question-plan.js";
8
8
  import { searchCombined } from "./messages-combined.js";
9
9
  import { prepareLucene, searchLucene } from "./messages-search.js";
10
10
  /** How many of the messages nearest in meaning join the fusion: the benchmark's pool (performance/search/evidence-pipeline). */
@@ -49,19 +49,8 @@ const EVERY_MESSAGE = {
49
49
  upper_inclusive: true,
50
50
  };
51
51
  const scopeOf = (root) => root === undefined ? {} : { ast: { version: 1, language: "lucene-v1", root } };
52
- /**
53
- * The query discovery searches. A question's own "?" would parse as a one-character wildcard and send it to strict
54
- * search (BUG-395); the colon of a time or ratio ("22:00", "1:1") would parse as a field and fail the whole search,
55
- * and escaped it is syntax again, so it splits the numbers (BUG-450). A quoted timestamp keeps its colons (BUG-458).
56
- */
57
- export const discoveryRequest = (asked) => asked.ast !== undefined || asked.exact || !asked.text
58
- ? asked
59
- : {
60
- ...asked,
61
- text: asked.text
62
- .replace(/(?<=\p{L})\?(?=\s|$)/gu, "")
63
- .replace(/"(?:[^"\\]|\\.)*"|(?<=\d):(?=\d)/gu, (found) => (found === ":" ? " " : found)),
64
- };
52
+ /** The query discovery searches: a question's text cleaned as `questionText` says. */
53
+ export const discoveryRequest = (asked) => asked.ast !== undefined || asked.exact || !asked.text ? asked : { ...asked, text: questionText(asked.text) };
65
54
  export const searchDiscovery = async (store, account, asked, messenger = {}, meaning, rerank) => {
66
55
  const request = discoveryRequest(asked);
67
56
  const original = await prepareLucene(store, account, request, messenger);
@@ -82,9 +71,7 @@ export const searchDiscovery = async (store, account, asked, messenger = {}, mea
82
71
  const question = root === undefined ? text : meaningWords(root);
83
72
  if (!question)
84
73
  return searchLucene(store, account, request, messenger);
85
- const terms = root === undefined
86
- ? [...new Set(plan.terms.concat(plan.aliases.flatMap((alias) => words(alias.split(" -> ")[1] ?? ""))))]
87
- : [...new Set(words(question))];
74
+ const terms = root === undefined ? lookupTerms(plan) : [...new Set(words(question))];
88
75
  if (!terms.length)
89
76
  return searchLucene(store, account, request, messenger);
90
77
  if (terms.length > 64)
@@ -125,7 +112,7 @@ export const searchDiscovery = async (store, account, asked, messenger = {}, mea
125
112
  truncated ||= found.hasMore || found.query.combined.truncated;
126
113
  }
127
114
  check();
128
- const relaxed = root === undefined ? `(${terms.join(" OR ")})${plan.scope ? ` ${plan.scope}` : ""}` : text;
115
+ const relaxed = root === undefined ? anyOfTerms(terms, plan.scope) : text;
129
116
  const partialRequest = { ...request, text: relaxed, ast: undefined, limit: 300, context: 0, thread: undefined };
130
117
  const partial = await searchLucene(store, account, partialRequest, messenger);
131
118
  add(partial.items);
@@ -16,9 +16,19 @@ export const meaningLane = (deps) => async (question, accounts, shared, within)
16
16
  const store = await deps.store();
17
17
  const ready = [];
18
18
  const without = [];
19
- for (const one of accounts)
20
- ((await store.vectors.hasMessageVectors(one, target.key)) ? ready : without).push(one);
21
- if (ready.length === 0)
19
+ // Mail keeps its vectors by thread, not by message: its accounts are read through the mail store.
20
+ const mail = new Map();
21
+ const stored = await store.accounts.storedAccounts();
22
+ for (const one of accounts) {
23
+ const id = one.provider === "email"
24
+ ? stored.find(({ provider, account }) => provider === one.provider && account === one.account)?.id
25
+ : undefined;
26
+ if (id !== undefined)
27
+ mail.set(one, Number(id));
28
+ else
29
+ ((await store.vectors.hasMessageVectors(one, target.key)) ? ready : without).push(one);
30
+ }
31
+ if (ready.length === 0 && mail.size === 0)
22
32
  return skipped("no message has a vector yet", "conversations.rebuild");
23
33
  const words = plainWords(question);
24
34
  try {
@@ -38,12 +48,24 @@ export const meaningLane = (deps) => async (question, accounts, shared, within)
38
48
  for (const { chatId, messageId, score } of near)
39
49
  ranked.push({ ...one, chatId, id: messageId, score });
40
50
  }
51
+ for (const [one, accountId] of mail) {
52
+ const near = await store.mail.nearest(target.key, query, {
53
+ accountId,
54
+ limit: MEANING_DEPTH,
55
+ });
56
+ if (near.length === 0)
57
+ without.push(one);
58
+ for (const { threadExternalId, externalId, score } of near)
59
+ ranked.push({ ...one, chatId: threadExternalId, id: externalId, score });
60
+ }
41
61
  };
42
62
  if (shared && !deps.embedders)
43
63
  await lookUp(await shared.get(target.key, () => target.open()));
44
64
  else
45
65
  await withEmbedder(deps, target, DEFAULT_TEXT_MODEL, {}, lookUp);
46
66
  }
67
+ if (ranked.length === 0 && without.length === accounts.length)
68
+ return skipped("no message has a vector yet", "conversations.rebuild");
47
69
  return {
48
70
  model: target.id,
49
71
  ranked: ranked.sort((a, b) => b.score - a.score).slice(0, MEANING_DEPTH),
@@ -40,7 +40,7 @@ export const messagesService = (deps) => {
40
40
  return;
41
41
  await ensureAccount({ text: target.text, account: target.account }, { account: account.account, ...(deps.profile === undefined ? {} : { profile: deps.profile }) });
42
42
  };
43
- const serverThenStore = async (store, account, query) => {
43
+ const serverThenStore = async (store, account, query, lanes = true) => {
44
44
  const server = await searchServer(deps, query);
45
45
  const only = query.backend === "server" && server
46
46
  ? [...server.sources.keys()].map((key) => {
@@ -48,12 +48,13 @@ export const messagesService = (deps) => {
48
48
  return { ...account, chatId, id };
49
49
  })
50
50
  : undefined;
51
- const found = await searchStore(store, account, only ? { ...query, only } : query, deps.messenger, query.discover && query.kind !== "mail" ? meaningLane(deps) : undefined, query.discover && query.kind !== "mail" ? rerankLane(deps) : undefined);
51
+ const found = await searchStore(store, account, only ? { ...query, only } : query, deps.messenger, lanes && query.discover && query.kind !== "mail" ? meaningLane(deps) : undefined, lanes && query.discover && query.kind !== "mail" ? rerankLane(deps) : undefined);
52
52
  return { found, server };
53
53
  };
54
54
  const unifiedMessages = (store, account) => async (query) => {
55
55
  await topUp(store);
56
- const { found, server } = await serverThenStore(store, account, query);
56
+ // `search all` adds meaning itself, and a reranker's load would cost it seconds on every question.
57
+ const { found, server } = await serverThenStore(store, account, query, false);
57
58
  return withServer(found, account, server);
58
59
  };
59
60
  /**
@@ -105,6 +105,8 @@ export interface FoundNote {
105
105
  }[];
106
106
  /** Which halves of the search found it. */
107
107
  found_by: ("title" | "words" | "meaning")[];
108
+ /** The cosine of the nearest chunk, when meaning found it: comparable with other records under the same model. */
109
+ meaning_score?: number;
108
110
  }
109
111
  export interface LinkedRecord {
110
112
  /** `person:…`, `note:…`, `entity:…`, or `null` while the name written in the notes matches nobody yet. */
@@ -147,6 +149,8 @@ export interface NotesQuery {
147
149
  withInternal?: boolean;
148
150
  source?: Note["source"];
149
151
  exact?: boolean;
152
+ /** A question typed as a sentence finds what holds some of its words, as `search messages --discover` does. */
153
+ discover?: boolean;
150
154
  timezone?: string;
151
155
  env?: NodeJS.ProcessEnv;
152
156
  /** The tool's command, named in the hint when the model is missing. */
@@ -182,7 +186,7 @@ export declare const RANK_WITHIN = 2000;
182
186
  * each hit says which half found it, and the answer lists what the notes found link to. Every field of the query and
183
187
  * of `filter` holds for every hit, found by words or by meaning; a query of fields alone is not searched by meaning.
184
188
  */
185
- export declare const searchNotes: (store: MessageStore, query: string, { limit, offset, tag, filter: expression, folderIds, withInternal, source: given, exact, timezone, env, command, embedders, pictures: withPictures, pictureLimit, pictureEmbedder, }?: NotesQuery) => Promise<NotesFound>;
189
+ export declare const searchNotes: (store: MessageStore, query: string, { limit, offset, tag, filter: expression, folderIds, withInternal, source: given, exact, discover, timezone, env, command, embedders, pictures: withPictures, pictureLimit, pictureEmbedder, }?: NotesQuery) => Promise<NotesFound>;
186
190
  /** What `notes:internal` and `notes:<folder id>` ask of the store: one query, so a page of it is one page. */
187
191
  export declare const notesScopeOf: (accounts: readonly AccountKey[]) => Pick<NotesQuery, "folderIds" | "withInternal" | "source">;
188
192
  export interface NotesFoundOf extends NotesFound {
@@ -10,6 +10,7 @@ import { fieldsOfQuery as fieldsOf, meaningWords } from "../search/lucene/meanin
10
10
  import { validateFields } from "../search/lucene/registry.js";
11
11
  import { hasStems } from "../search/lucene/resolved.js";
12
12
  import { isQueryLimit, SEARCH_QUERY } from "../search/lucene/types.js";
13
+ import { questionLookup, questionText } from "../search/question-plan.js";
13
14
  import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
14
15
  import { TOTAL_CAP, totalOf } from "../search/total.js";
15
16
  import { openPictureEmbedderFor } from "./document-pictures.js";
@@ -324,20 +325,22 @@ const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others
324
325
  * each hit says which half found it, and the answer lists what the notes found link to. Every field of the query and
325
326
  * of `filter` holds for every hit, found by words or by meaning; a query of fields alone is not searched by meaning.
326
327
  */
327
- export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, filter: expression, folderIds, withInternal, source: given, exact = false, timezone, env, command, embedders, pictures: withPictures = false, pictureLimit = 5, pictureEmbedder, } = {}) => {
328
+ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, filter: expression, folderIds, withInternal, source: given, exact = false, discover = false, timezone, env, command, embedders, pictures: withPictures = false, pictureLimit = 5, pictureEmbedder, } = {}) => {
328
329
  const typed = noteSourceOfQuery(query);
329
330
  const source = given ?? typed.source;
330
331
  const rest = typed.source !== undefined && typed.source === source ? typed.rest : query;
331
332
  const label = tag === undefined ? undefined : normalizeTag(tag);
332
333
  const filter = [expression ? `(${expression})` : "", label === undefined ? "" : `tag:${label}`].filter(Boolean).join(" AND ") ||
333
334
  undefined;
334
- const parsed = query.trim() ? parsedQuery(query, exact) : undefined;
335
+ const lookup = discover && !exact ? questionLookup(query) : undefined;
336
+ const parsed = query.trim() ? parsedQuery(lookup === undefined ? query : questionText(query), exact) : undefined;
335
337
  const words = parsed === undefined ? "" : meaningWords(parsed);
336
338
  const narrowing = allOf([
337
339
  ...(parsed === undefined ? [] : [fieldsOfQuery(parsed)].filter((node) => node !== undefined)),
338
340
  ...(filter === undefined ? [] : [parsedQuery(filter)]),
339
341
  ]);
340
- const text = [query.trim() ? `(${query})` : "", filter ?? ""].filter(Boolean).join(" AND ");
342
+ const asked = lookup ?? query;
343
+ const text = [asked.trim() ? `(${asked})` : "", filter ?? ""].filter(Boolean).join(" AND ");
341
344
  const window = offset + limit;
342
345
  const scope = {
343
346
  ...(folderIds === undefined ? {} : { folderIds }),
@@ -347,6 +350,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
347
350
  };
348
351
  let meaningSkipped;
349
352
  let meaning;
353
+ const cosines = new Map();
350
354
  /** Every note found by meaning; `meaningComplete` is false when the nearest overall were cut at the depth. */
351
355
  let meaningRefs;
352
356
  let meaningComplete = true;
@@ -375,8 +379,10 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
375
379
  nearest = ranked.slice(0, depth);
376
380
  }
377
381
  const allowed = await matching(store, nearest.map(({ ref }) => ref), narrowing, scope);
378
- meaning = nearest.flatMap(({ ref }) => {
382
+ meaning = nearest.flatMap(({ ref, score }) => {
379
383
  const found = allowed.get(ref);
384
+ if (found)
385
+ cosines.set(ref, score);
380
386
  return found ? [{ ref, ...found }] : [];
381
387
  });
382
388
  meaningRefs ??= new Set(meaning.map(({ ref }) => ref));
@@ -410,9 +416,13 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
410
416
  if (tierRefs.has(ref))
411
417
  return;
412
418
  const held = ranked.get(ref) ?? { note, at, score: 0, foundBy: [] };
413
- held.score += 1 / (RRF_K + rank + 1);
414
419
  held.foundBy.push(by);
415
420
  ranked.set(ref, held);
421
+ // A question's any-word list holds nearly every note meaning finds; a second vote there buried notes found by
422
+ // words alone (recall@10 0.55 against 1.00, cli-testing knowledge-graph-memory, MEMO-36).
423
+ if (by === "meaning" && lookup !== undefined && held.foundBy.includes("words"))
424
+ return;
425
+ held.score += 1 / (RRF_K + rank + 1);
416
426
  });
417
427
  add(matched.items, "words");
418
428
  if (meaning !== undefined)
@@ -446,6 +456,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
446
456
  excerpt: firstLine(note.text, words),
447
457
  links,
448
458
  found_by: foundBy,
459
+ ...(cosines.has(ref) ? { meaning_score: cosines.get(ref) } : {}),
449
460
  });
450
461
  for (const link of new Map(links.map((link) => [link.to ?? `?${link.target_text}`, link])).values()) {
451
462
  const key = link.to ?? `?${link.target_text}`;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Proposal ids from the command line, before any reference parsing: `17070-17079,fact:500-502,17080/f1` becomes
3
- * each id once, in the order given.
2
+ * Proposal ids from the command line, before any reference parsing: `17070-17079,fact:500-502,17080/f1,close:Q-15`
3
+ * becomes each id once, in the order given. A task key after a prefix is one id, never a range.
4
4
  */
5
5
  export declare const parseProposalIds: (text: readonly string[]) => string[];
@@ -18,6 +18,8 @@ const one = (token) => {
18
18
  const part = /^([1-9]\d*)\/([A-Za-z0-9_-]{1,64})$/.exec(token);
19
19
  if (part)
20
20
  return [token];
21
+ if (/^[a-z][a-z_]*:[A-Za-z][A-Za-z0-9]*-[1-9]\d*$/.test(token))
22
+ return [token];
21
23
  const prefixed = /^([a-z][a-z_]*):([1-9]\d*)(?:-(?:([a-z][a-z_]*):)?([1-9]\d*))?$/.exec(token);
22
24
  if (prefixed?.[1] && prefixed[2]) {
23
25
  const [, prefix, from, other, to] = prefixed;
@@ -27,11 +29,11 @@ const one = (token) => {
27
29
  }
28
30
  if (/^[1-9]\d*-[a-z]/.test(token))
29
31
  return refuse(`a range stays inside one prefix: ${token}`);
30
- return refuse(`not a proposal id: ${token}; use 17070, 17070-17079, fact:512 or 17080/f2`);
32
+ return refuse(`not a proposal id: ${token}; use 17070, 17070-17079, fact:512, close:Q-15 or 17080/f2`);
31
33
  };
32
34
  /**
33
- * Proposal ids from the command line, before any reference parsing: `17070-17079,fact:500-502,17080/f1` becomes
34
- * each id once, in the order given.
35
+ * Proposal ids from the command line, before any reference parsing: `17070-17079,fact:500-502,17080/f1,close:Q-15`
36
+ * becomes each id once, in the order given. A task key after a prefix is one id, never a range.
35
37
  */
36
38
  export const parseProposalIds = (text) => {
37
39
  const ids = [...new Set(text.flatMap((arg) => arg.split(",")).flatMap((token) => one(token.trim())))];
@@ -5,6 +5,6 @@ import type { ProposalKind } from "../types.js";
5
5
  export declare const FACT_SUMMARIES: Record<string, (finding: Finding) => string>;
6
6
  /**
7
7
  * An agent's batch of cited facts and tasks from stored messages, mail and documents. Confirming accepts the parts
8
- * named, or every finding not rejected before; the rest are rejected. Meeting findings stay with their meeting.
8
+ * named, or every finding not rejected before; the rest are rejected. Meeting findings have their own kind.
9
9
  */
10
10
  export declare const findingsKind: (store: Pick<MessageStore, "evidenceFindings" | "proposedActions">) => ProposalKind;
@@ -35,7 +35,7 @@ const rowOf = (batch, createdAt) => {
35
35
  };
36
36
  /**
37
37
  * An agent's batch of cited facts and tasks from stored messages, mail and documents. Confirming accepts the parts
38
- * named, or every finding not rejected before; the rest are rejected. Meeting findings stay with their meeting.
38
+ * named, or every finding not rejected before; the rest are rejected. Meeting findings have their own kind.
39
39
  */
40
40
  export const findingsKind = (store) => {
41
41
  // The batch view carries no staging time; its stored proposal does.
@@ -0,0 +1,7 @@
1
+ import { type MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ /**
4
+ * A meeting's cited findings, staged by `meetings actions add`: the same review as `meetings actions confirm`, with
5
+ * the batch's current fingerprint. Only the owner decides it.
6
+ */
7
+ export declare const meetingFindingsKind: (store: Pick<MessageStore, "meetingFindings" | "proposedActions">) => ProposalKind;
@@ -0,0 +1,73 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { MEETING_FINDINGS_KIND } from "../../../meetings/index.js";
3
+ import { proposerOfAction, } from "../../../store/index.js";
4
+ import { FACT_SUMMARIES } from "./findings.js";
5
+ import { kept } from "./shared.js";
6
+ const rowOf = (action, batch) => {
7
+ const findings = batch.input.findings;
8
+ const projects = [...new Set(findings.map((one) => ("project" in one ? one.project : undefined)))];
9
+ const outcome = (id) => batch.receipt?.items.find((item) => item.id === id)?.status ?? null;
10
+ const references = [...new Set(findings.flatMap((finding) => finding.evidence.map((one) => one.reference)))];
11
+ return {
12
+ id: action.id,
13
+ kind: "meeting_findings",
14
+ status: batch.status === "proposed" ? "proposed" : batch.receipt?.accepted.length ? "confirmed" : "rejected",
15
+ project: projects.length === 1 && typeof projects[0] === "string" ? projects[0] : null,
16
+ subject: references[0] ?? `meeting_findings:${action.id}`,
17
+ object: null,
18
+ summary: `${findings.length} finding${findings.length === 1 ? "" : "s"}`,
19
+ proposedBy: proposerOfAction(action),
20
+ reason: action.reason,
21
+ evidence: references.slice(0, 5),
22
+ score: null,
23
+ signals: null,
24
+ parts: findings.map((finding) => ({
25
+ id: finding.id,
26
+ kind: finding.kind,
27
+ summary: FACT_SUMMARIES[finding.kind]?.(finding) ?? finding.kind,
28
+ outcome: outcome(finding.id),
29
+ })),
30
+ createdAt: action.createdAt,
31
+ };
32
+ };
33
+ /**
34
+ * A meeting's cited findings, staged by `meetings actions add`: the same review as `meetings actions confirm`, with
35
+ * the batch's current fingerprint. Only the owner decides it.
36
+ */
37
+ export const meetingFindingsKind = (store) => {
38
+ const batchOf = async (action) => {
39
+ if (action.account === null)
40
+ throw new CliError("not_found", `meeting findings ${action.id} name no account`);
41
+ return { account: action.account, batch: await store.meetingFindings.get(action.account, action.id) };
42
+ };
43
+ return {
44
+ kind: "meeting_findings",
45
+ stored: [MEETING_FINDINGS_KIND],
46
+ parts: true,
47
+ ownerOnly: true,
48
+ list: async (filter) => {
49
+ const actions = await store.proposedActions.list({
50
+ kinds: [MEETING_FINDINGS_KIND],
51
+ ...((filter.status ?? "proposed") === "proposed" ? { status: "proposed" } : {}),
52
+ ...(filter.by === undefined ? {} : { proposedBy: filter.by }),
53
+ limit: Math.min(filter.offset + filter.limit, 500),
54
+ });
55
+ const rows = await Promise.all(actions.map(async (action) => rowOf(action, (await batchOf(action)).batch)));
56
+ return kept(rows, { ...filter, limit: filter.offset + filter.limit });
57
+ },
58
+ get: async (id) => {
59
+ const action = await store.proposedActions.get(id);
60
+ return rowOf(action, (await batchOf(action)).batch);
61
+ },
62
+ decide: async (id, { confirm, by, parts }) => {
63
+ const { account, batch } = await batchOf(await store.proposedActions.get(id));
64
+ const turnedDown = new Set(batch.previous.filter((one) => one.status === "rejected").map((one) => one.id));
65
+ const accept = !confirm
66
+ ? []
67
+ : (parts ?? batch.input.findings.map((finding) => finding.id).filter((one) => !turnedDown.has(one)));
68
+ const reviewed = await store.meetingFindings.review(account, id, { fingerprint: batch.fingerprint, accept, by });
69
+ const made = (reviewed.receipt?.items ?? []).flatMap((item) => item.status === "accepted" && item.reference && !item.reused ? [item.reference] : []);
70
+ return { id, kind: "meeting_findings", status: accept.length ? "confirmed" : "rejected", made };
71
+ },
72
+ };
73
+ };
@@ -0,0 +1,7 @@
1
+ import type { MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ /**
4
+ * An agent's close of an open task, waiting on the owner (`close_suggested_*`). Confirming closes it as suggested;
5
+ * rejecting clears the suggestion, as `tasks reject` does. Its id is `close:<task key>`; only the owner decides it.
6
+ */
7
+ export declare const taskCloseKind: (store: Pick<MessageStore, "tasks">) => ProposalKind;
@@ -0,0 +1,76 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { deciderAuthor, TASK_CLOSE_KIND } from "../../../store/decisions.js";
3
+ import { createTaskService } from "../../../tasks/index.js";
4
+ import { kept } from "./shared.js";
5
+ const rowOf = (task) => {
6
+ const suggestion = task.closeSuggestion;
7
+ if (suggestion === null)
8
+ return undefined;
9
+ return {
10
+ id: `close:${task.key}`,
11
+ kind: TASK_CLOSE_KIND,
12
+ status: "proposed",
13
+ project: task.project,
14
+ subject: task.ref,
15
+ object: null,
16
+ summary: `close as ${suggestion.as}: ${task.title}`,
17
+ proposedBy: { type: "bot", id: suggestion.by, name: suggestion.by, kind: "agent" },
18
+ reason: suggestion.reason,
19
+ evidence: task.source === null ? [] : [task.source],
20
+ score: suggestion.certainty,
21
+ signals: null,
22
+ parts: null,
23
+ createdAt: suggestion.at,
24
+ };
25
+ };
26
+ /**
27
+ * An agent's close of an open task, waiting on the owner (`close_suggested_*`). Confirming closes it as suggested;
28
+ * rejecting clears the suggestion, as `tasks reject` does. Its id is `close:<task key>`; only the owner decides it.
29
+ */
30
+ export const taskCloseKind = (store) => {
31
+ const taskOf = async (id) => {
32
+ const key = id.slice("close:".length);
33
+ const task = await store.tasks.details(key).catch(() => undefined);
34
+ const row = task && rowOf(task);
35
+ if (!task || !row)
36
+ throw new CliError("not_found", `no close waits on task ${key}`);
37
+ return { task, row };
38
+ };
39
+ return {
40
+ kind: TASK_CLOSE_KIND,
41
+ prefix: "close",
42
+ ownerOnly: true,
43
+ list: async (filter) => {
44
+ if ((filter.status ?? "proposed") !== "proposed" && filter.status !== "all")
45
+ return [];
46
+ const tasks = await store.tasks.find({
47
+ status: "open",
48
+ where: { sql: "t.close_suggested_as IS NOT NULL", params: [] },
49
+ limit: filter.offset + filter.limit,
50
+ });
51
+ return kept(tasks.flatMap((task) => rowOf(task) ?? []), { ...filter, limit: filter.offset + filter.limit });
52
+ },
53
+ get: async (id) => (await taskOf(id)).row,
54
+ decide: async (id, { confirm, by, reason }) => {
55
+ const { task } = await taskOf(id);
56
+ const suggestion = task.closeSuggestion;
57
+ if (!confirm || suggestion === null) {
58
+ await store.tasks.suggestClose(task.id, null, "owner");
59
+ return { id, kind: TASK_CLOSE_KIND, status: "rejected", made: [] };
60
+ }
61
+ // A project task has no row in the message task service; a message task closes there, keeping the
62
+ // suggestion's reason as `tasks close` does.
63
+ if ((await store.tasks.get(task.id)) === undefined)
64
+ await store.tasks.edit(task.id, { status: suggestion.as, ...(reason === undefined ? {} : { reason }) }, deciderAuthor(by));
65
+ else {
66
+ const why = reason ?? suggestion.reason ?? undefined;
67
+ await createTaskService({ store: store.tasks }).close(task.id, {
68
+ as: suggestion.as,
69
+ by: "owner",
70
+ ...(why === undefined ? {} : { reason: why }),
71
+ });
72
+ }
73
+ return { id, kind: TASK_CLOSE_KIND, status: "confirmed", made: [task.ref] };
74
+ },
75
+ };
76
+ };
@@ -6,9 +6,11 @@ import { invoiceKind, paymentKind } from "./kinds/finance.js";
6
6
  import { findingsKind } from "./kinds/findings.js";
7
7
  import { keyDocumentKind } from "./kinds/key-document.js";
8
8
  import { mailFindingsKind } from "./kinds/mail-findings.js";
9
+ import { meetingFindingsKind } from "./kinds/meeting-findings.js";
9
10
  import { memoryKind } from "./kinds/memory.js";
10
11
  import { relationshipKind } from "./kinds/relationship.js";
11
12
  import { relationshipUpdateKind } from "./kinds/relationship-update.js";
13
+ import { taskCloseKind } from "./kinds/task-close.js";
12
14
  const PREFIX = /^([a-z][a-z_]*):/;
13
15
  const marked = (kind, row) => kind.ownerOnly ? { ...row, ownerOnly: true } : row;
14
16
  const refusalOf = (id, error) => {
@@ -133,6 +135,8 @@ export const proposalRegistry = (store) => {
133
135
  memoryKind(store),
134
136
  findingsKind(store),
135
137
  mailFindingsKind(store),
138
+ meetingFindingsKind(store),
139
+ taskCloseKind(store),
136
140
  ])
137
141
  registry.register(kind);
138
142
  registry.register(actionKind(store, registry.claimed));
@@ -79,6 +79,11 @@ export interface SearchAllFound {
79
79
  by: "words" | "words and meaning";
80
80
  meaningSkipped?: MeaningSkipped;
81
81
  };
82
+ /** How the mail was searched, when it was; never by meaning with `exact`. */
83
+ mail?: {
84
+ by: "words" | "words and meaning";
85
+ meaningSkipped?: MeaningSkipped;
86
+ };
82
87
  /** The messenger's server step for messages, as `search messages` reports it. */
83
88
  server?: ServerSearched;
84
89
  /** What the store held of the chats the messages were searched in; absent when messages were not searched. */