@wirecat/cli-core 0.548.0 → 0.550.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 (72) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/dist/calendar/cli/index.d.ts +10 -1
  3. package/dist/calendar/cli/index.js +9 -3
  4. package/dist/calendar/cli/search-all.d.ts +17 -0
  5. package/dist/calendar/cli/search-all.js +72 -0
  6. package/dist/cli/answer-entity.d.ts +4 -0
  7. package/dist/cli/answer-entity.js +5 -0
  8. package/dist/cli/bot/context.d.ts +2 -0
  9. package/dist/cli/compact.d.ts +41 -0
  10. package/dist/cli/compact.js +313 -0
  11. package/dist/cli/context.d.ts +11 -2
  12. package/dist/cli/context.js +15 -6
  13. package/dist/cli/envelope.d.ts +5 -1
  14. package/dist/cli/envelope.js +15 -4
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +2 -0
  17. package/dist/cli/messenger/search-command.d.ts +10 -0
  18. package/dist/cli/messenger/search-command.js +19 -13
  19. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  20. package/dist/cli/program.d.ts +3 -0
  21. package/dist/cli/program.js +7 -1
  22. package/dist/domain/providers.d.ts +3 -0
  23. package/dist/domain/providers.js +20 -4
  24. package/dist/domain/ref-arguments.js +3 -7
  25. package/dist/domain/ref-summaries.d.ts +56 -0
  26. package/dist/domain/ref-summaries.js +63 -0
  27. package/dist/entities/index.d.ts +11 -0
  28. package/dist/entities/relationship.d.ts +2 -0
  29. package/dist/entities/relationship.js +2 -0
  30. package/dist/entities/task-results.d.ts +10 -0
  31. package/dist/entities/task-results.js +1 -0
  32. package/dist/errors/reasons.js +2 -0
  33. package/dist/mail/partial.d.ts +9 -3
  34. package/dist/mail/partial.js +35 -10
  35. package/dist/mail/source.d.ts +7 -1
  36. package/dist/mail/sync.js +1 -1
  37. package/dist/output-detail.js +9 -5
  38. package/dist/output.d.ts +5 -1
  39. package/dist/output.js +29 -4
  40. package/dist/services/proposals/kinds/relationship-update.d.ts +4 -0
  41. package/dist/services/proposals/kinds/relationship-update.js +53 -0
  42. package/dist/services/proposals/registry.js +2 -0
  43. package/dist/store/decisions.d.ts +3 -1
  44. package/dist/store/decisions.js +7 -1
  45. package/dist/store/index.d.ts +2 -1
  46. package/dist/store/sqlite/email-locations.d.ts +22 -1
  47. package/dist/store/sqlite/email-locations.js +29 -2
  48. package/dist/store/sqlite/emails.d.ts +3 -7
  49. package/dist/store/sqlite/emails.js +4 -2
  50. package/dist/store/sqlite/evidence-sources.d.ts +2 -2
  51. package/dist/store/sqlite/evidence-sources.js +41 -77
  52. package/dist/store/sqlite/knowledge.d.ts +17 -23
  53. package/dist/store/sqlite/knowledge.js +13 -13
  54. package/dist/store/sqlite/manifest.js +2 -0
  55. package/dist/store/sqlite/migrations.generated.js +12 -0
  56. package/dist/store/sqlite/notes.d.ts +5 -0
  57. package/dist/store/sqlite/notes.js +6 -2
  58. package/dist/store/sqlite/open-store.js +2 -0
  59. package/dist/store/sqlite/proposed-actions.d.ts +1 -1
  60. package/dist/store/sqlite/proposed-actions.js +10 -2
  61. package/dist/store/sqlite/ref-summaries.d.ts +17 -0
  62. package/dist/store/sqlite/ref-summaries.js +378 -0
  63. package/dist/store/sqlite/relation-proposals.d.ts +40 -0
  64. package/dist/store/sqlite/relation-proposals.js +88 -1
  65. package/dist/store/sqlite/schema.d.ts +16 -0
  66. package/dist/store/sqlite/schema.js +1 -0
  67. package/dist/store/sqlite/suggested-links.d.ts +10 -0
  68. package/dist/store/sqlite/suggested-links.js +68 -0
  69. package/dist/store/store.d.ts +3 -0
  70. package/dist/store/store.js +5 -0
  71. package/package.json +3 -3
  72. package/parity.json +4 -0
@@ -2,13 +2,17 @@ export const DETAILS = ["brief", "full"];
2
2
  /** Message fields mail never has and a messenger hit rarely fills; a null one is noise. */
3
3
  const RARELY_SET = ["editedAt", "replyTo", "forwardedFrom", "reactions"];
4
4
  const BRIEF = ["locator", "chatTitle", "timestamp", "senderName", "excerpt", "score"];
5
- const isHit = (value) => typeof value === "object" &&
6
- value !== null &&
7
- typeof value.locator === "string" &&
8
- typeof value.text === "string";
5
+ /** A messenger answer's hit (`message.hit`): already cut to an excerpt, so only `brief` changes it. */
6
+ const SNAKE_BRIEF = ["ref", "chat_title", "timestamp", "sender_name", "excerpt", "score"];
7
+ const isRecord = (value) => typeof value === "object" && value !== null;
8
+ const isSnakeHit = (value) => isRecord(value) && typeof value.ref === "string" && typeof value.excerpt === "string";
9
+ const isHit = (value) => isSnakeHit(value) || (isRecord(value) && typeof value.locator === "string" && typeof value.text === "string");
10
+ const picked = (hit, keys) => Object.fromEntries(keys.flatMap((key) => (hit[key] === undefined || hit[key] === null ? [] : [[key, hit[key]]])));
9
11
  const shaped = (hit, detail) => {
12
+ if (isSnakeHit(hit))
13
+ return detail === "brief" ? picked(hit, SNAKE_BRIEF) : hit;
10
14
  if (detail === "brief")
11
- return Object.fromEntries(BRIEF.flatMap((key) => (hit[key] === undefined || hit[key] === null ? [] : [[key, hit[key]]])));
15
+ return picked(hit, BRIEF);
12
16
  const { text, providerMetadata, ...rest } = hit;
13
17
  for (const key of RARELY_SET)
14
18
  if (rest[key] === null)
package/dist/output.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface OutputOptions {
14
14
  hitDetail?: Detail;
15
15
  /** Takes the answer as an object in place of printing it — an agent's run (`BaseEnvironment.agent`). */
16
16
  capture?: (value: unknown) => void;
17
+ /** One line per item (WC-OUT-24); anything printed past `renderAnswer` stays JSON. */
18
+ compact?: boolean;
17
19
  }
18
20
  /**
19
21
  * One place decides the mode, so no command can disagree with another.
@@ -23,9 +25,11 @@ export interface OutputOptions {
23
25
  * instead. In every machine mode **stdout carries JSON and nothing else** — diagnostics are on stderr in all modes, which is what makes
24
26
  * that contract hold by construction rather than by remembering.
25
27
  */
26
- export declare const resolveOutput: ({ json, jsonl, quiet, streams, tty, color, fields, hitDetail, env, capture, }?: OutputOptions) => {
28
+ export declare const resolveOutput: ({ json, jsonl, quiet, streams, tty, color, fields, hitDetail, env, capture, compact, }?: OutputOptions) => {
27
29
  format: "json" | "jsonl" | "pretty";
28
30
  color: boolean;
29
31
  streams: Streams;
30
32
  renderer: Renderer;
33
+ compact?: boolean | undefined;
34
+ full?: boolean | undefined;
31
35
  };
package/dist/output.js CHANGED
@@ -1,6 +1,23 @@
1
- import { createRenderer, processStreams, singleLine, } from "@wirecat/cli-base";
1
+ import { CliError, createRenderer, processStreams, singleLine, } from "@wirecat/cli-base";
2
+ import { COMPACT_FLAG } from "@wirecat/cli-base/standards";
2
3
  import { fieldsOf, projectFields } from "./cli/result-fields.js";
3
4
  import { atDetail } from "./output-detail.js";
5
+ const refuseCompact = ({ json, jsonl, fields, hitDetail }) => {
6
+ const other = jsonl
7
+ ? "--jsonl"
8
+ : fields !== undefined
9
+ ? "--json=…"
10
+ : json
11
+ ? "--json"
12
+ : hitDetail === "brief"
13
+ ? "--brief"
14
+ : "";
15
+ if (other)
16
+ throw new CliError("validation_error", `${COMPACT_FLAG} and ${other} ask for different output — pick one`, {
17
+ reason: "output.conflict",
18
+ param: COMPACT_FLAG,
19
+ });
20
+ };
4
21
  /**
5
22
  * One place decides the mode, so no command can disagree with another.
6
23
  *
@@ -9,9 +26,11 @@ import { atDetail } from "./output-detail.js";
9
26
  * instead. In every machine mode **stdout carries JSON and nothing else** — diagnostics are on stderr in all modes, which is what makes
10
27
  * that contract hold by construction rather than by remembering.
11
28
  */
12
- export const resolveOutput = ({ json, jsonl, quiet, streams = processStreams, tty, color, fields, hitDetail, env = process.env, capture, } = {}) => {
29
+ export const resolveOutput = ({ json, jsonl, quiet, streams = processStreams, tty, color, fields, hitDetail, env = process.env, capture, compact, } = {}) => {
30
+ if (compact)
31
+ refuseCompact({ json, jsonl, fields, hitDetail });
13
32
  const interactive = tty ?? process.stdout.isTTY === true;
14
- const format = jsonl ? "jsonl" : json || !interactive ? "json" : "pretty";
33
+ const format = jsonl ? "jsonl" : json || compact || !interactive ? "json" : "pretty";
15
34
  const painted = format === "pretty" && (color ?? (env.NO_COLOR === undefined && env.TERM !== "dumb" && !env.CI));
16
35
  const printing = createRenderer({ format, color: painted, streams });
17
36
  const rendered = capture
@@ -37,7 +56,13 @@ export const resolveOutput = ({ json, jsonl, quiet, streams = processStreams, tt
37
56
  stream: (items) => created.stream([...items].map((item) => projectFields(item, paths))),
38
57
  }
39
58
  : created;
40
- return { format, color: painted, streams, renderer: quiet ? silence(renderer) : renderer };
59
+ return {
60
+ format,
61
+ color: painted,
62
+ streams,
63
+ renderer: quiet ? silence(renderer) : renderer,
64
+ ...(compact ? { compact: true, full: hitDetail === "full" } : {}),
65
+ };
41
66
  };
42
67
  /** Fields a person writes in paragraphs; every other string is a name, a title, an id or a path. */
43
68
  const MULTI_LINE = new Set(["text", "description"]);
@@ -0,0 +1,4 @@
1
+ import type { MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ /** An agent's change to a relationship's role or purpose; the owner's applies at once and never waits here. */
4
+ export declare const relationshipUpdateKind: (store: Pick<MessageStore, "knowledge" | "proposedActions">) => ProposalKind;
@@ -0,0 +1,53 @@
1
+ import { proposerOfAction } from "../../../store/index.js";
2
+ import { actionStatus, kept, projectOf, pushed, storedStatus } from "./shared.js";
3
+ const KIND = "relationship_update";
4
+ const summaryOf = ({ changes }) => Object.entries(changes)
5
+ .map(([field, value]) => `${field} ${value ?? "cleared"}`)
6
+ .join(", ");
7
+ const rowOf = (action) => {
8
+ const payload = action.payload;
9
+ return {
10
+ id: action.id,
11
+ kind: KIND,
12
+ status: actionStatus(action.status),
13
+ project: projectOf(payload.to, payload.from),
14
+ subject: `relationship:${payload.relationship}`,
15
+ object: payload.to,
16
+ summary: summaryOf(payload),
17
+ proposedBy: proposerOfAction(action),
18
+ reason: action.reason,
19
+ evidence: [],
20
+ score: null,
21
+ signals: null,
22
+ parts: null,
23
+ createdAt: action.createdAt,
24
+ };
25
+ };
26
+ /** An agent's change to a relationship's role or purpose; the owner's applies at once and never waits here. */
27
+ export const relationshipUpdateKind = (store) => ({
28
+ kind: KIND,
29
+ stored: [KIND],
30
+ list: async (filter) => {
31
+ const status = storedStatus(filter);
32
+ const stored = await store.proposedActions.list({
33
+ kinds: [KIND],
34
+ ...(status === undefined ? {} : { status }),
35
+ ...pushed(filter),
36
+ });
37
+ return kept(stored.map(rowOf), filter);
38
+ },
39
+ get: async (id) => rowOf(await store.proposedActions.get(id)),
40
+ decide: async (id, { confirm, by, reason }) => {
41
+ const { relation } = await store.knowledge.decideRelationUpdate(id, {
42
+ confirm,
43
+ by,
44
+ ...(reason === undefined ? {} : { reason }),
45
+ });
46
+ return {
47
+ id,
48
+ kind: KIND,
49
+ status: confirm ? "confirmed" : "rejected",
50
+ made: relation ? [`relationship:${relation.id}`] : [],
51
+ };
52
+ },
53
+ });
@@ -6,6 +6,7 @@ import { findingsKind } from "./kinds/findings.js";
6
6
  import { keyDocumentKind } from "./kinds/key-document.js";
7
7
  import { memoryKind } from "./kinds/memory.js";
8
8
  import { relationshipKind } from "./kinds/relationship.js";
9
+ import { relationshipUpdateKind } from "./kinds/relationship-update.js";
9
10
  const PREFIX = /^([a-z][a-z_]*):/;
10
11
  const refusalOf = (id, error) => {
11
12
  if (!(error instanceof CliError))
@@ -118,6 +119,7 @@ export const proposalRegistry = (store) => {
118
119
  const registry = emptyProposalRegistry(store);
119
120
  for (const kind of [
120
121
  relationshipKind(store),
122
+ relationshipUpdateKind(store),
121
123
  keyDocumentKind(store),
122
124
  factKind(store),
123
125
  memoryKind(store),
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Who proposed something waiting for the owner. `unknown` is a suggestion that names no author, such as an
3
- * unconfirmed link written without provenance: only the owner decides it.
3
+ * unconfirmed link written without provenance: only the owner decides it. `verified: false` is a bot named only by
4
+ * free text anyone could write, as an old suggested link's provenance: only the owner decides that one too.
4
5
  */
5
6
  export type Proposer = {
6
7
  type: "person";
@@ -10,6 +11,7 @@ export type Proposer = {
10
11
  id: string;
11
12
  name: string;
12
13
  kind: "agent" | "rule";
14
+ verified?: false;
13
15
  } | {
14
16
  type: "unknown";
15
17
  };
@@ -20,6 +20,8 @@ export const mayDecide = (row, by, policy = DEFAULT_DECISION_POLICY) => {
20
20
  const proposer = row.proposedBy;
21
21
  if (proposer.type === "unknown")
22
22
  return refuse("permission_error", "proposal.owner-only", "this proposal names no author; only the owner decides it");
23
+ if (proposer.type === "bot" && proposer.verified === false)
24
+ return refuse("permission_error", "proposal.owner-only", `${proposer.name} is named only by an old link's provenance; only the owner decides it`);
23
25
  if (proposer.type === "bot" && proposer.kind === "rule" && policy.ruleProposalsOwnerOnly)
24
26
  return refuse("permission_error", "proposal.owner-only", `proposed by ${proposer.name}: only the owner decides it`);
25
27
  if (proposer.type === "bot" && proposer.kind === "agent" && botKey(proposer.name) === botKey(by.bot))
@@ -39,7 +41,11 @@ export const assertMayWithdraw = (row, by) => {
39
41
  if (by === "owner")
40
42
  return;
41
43
  const proposer = row.proposedBy;
42
- if (row.proposed && proposer.type === "bot" && proposer.kind === "agent" && botKey(proposer.name) === botKey(by.bot))
44
+ if (row.proposed &&
45
+ proposer.type === "bot" &&
46
+ proposer.kind === "agent" &&
47
+ proposer.verified !== false &&
48
+ botKey(proposer.name) === botKey(by.bot))
43
49
  return;
44
50
  throw new CliError("permission_error", `only the owner removes or reverses ${row.id}`, {
45
51
  reason: "proposal.owner-only",
@@ -1,5 +1,6 @@
1
1
  export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextSource, } from "../context/evidence.js";
2
2
  export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES, TASK_TYPES, WORK_TASK_TYPES, } from "../domain/knowledge.js";
3
+ export type { RefState, RefSummary, RefSummaryKind, RefsStore } from "../domain/ref-summaries.js";
3
4
  export type { TagConsolidationApplyOptions, TagConsolidationOptions, TagConsolidationPreview, TagConsolidationReceipt, TagConsolidationStore, } from "../domain/tag-consolidation.js";
4
5
  export type { TagRedirectIndex, TagRedirectResolution, TagRedirectSnapshot } from "../domain/tag-redirects.js";
5
6
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
@@ -51,7 +52,7 @@ export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, typ
51
52
  export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
52
53
  export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
53
54
  export { PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
54
- export type { LinkDecision } from "./sqlite/relation-proposals.js";
55
+ export type { LinkDecision, RelationUpdate } from "./sqlite/relation-proposals.js";
55
56
  export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchIndexState, searchIndexState, } from "./sqlite/search-index.js";
56
57
  export type { SyncStateSave } from "./sqlite/sync.js";
57
58
  export type { TagCreationOptions, TagCreationReview } from "./sqlite/tag-creation.js";
@@ -23,7 +23,8 @@ export declare const locate: (database: CacheDatabase, accountId: number, emailI
23
23
  export declare const folderEmails: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string) => FolderEmail[];
24
24
  /**
25
25
  * An email stored from its first bytes only (`metadata.partial`): a sync reads it whole and clears the mark.
26
- * `partialEmails` lists an account's live ones newest first, with how many there are in all.
26
+ * `partialEmails` lists an account's live ones newest first, with how many there are in all; one given up on
27
+ * (`partial.stuck`) is left out and only counted, so it never holds back newer ones. Its newest location comes first.
27
28
  */
28
29
  export interface PartialEmail {
29
30
  externalId: string;
@@ -35,6 +36,26 @@ export interface PartialEmail {
35
36
  export declare const partialEmails: (database: CacheDatabase, accountId: number, limit: number) => {
36
37
  emails: PartialEmail[];
37
38
  total: number;
39
+ stuck: number;
38
40
  };
41
+ /** Replaces a partial email's `metadata.partial`, as a failed read records itself; a whole email is left alone. */
42
+ export declare const markPartial: (database: CacheDatabase, accountId: number, externalId: string, mark: Metadata) => {
43
+ changes: number;
44
+ };
45
+ export interface PartialMailStore {
46
+ partialEmails(accountId: number, limit: number): Promise<{
47
+ emails: PartialEmail[];
48
+ total: number;
49
+ stuck: number;
50
+ }>;
51
+ /** Records a failed read of a partial email in its `metadata.partial`; a whole email is left alone. */
52
+ markPartial(accountId: number, externalId: string, mark: Metadata): Promise<void>;
53
+ }
54
+ export declare const partialMail: (database: CacheDatabase) => PartialMailStore;
55
+ /**
56
+ * A live email stored whole, which a partial copy must not replace: a server search checks what is held before its
57
+ * slow read, and a sync in another process may store the email whole in between.
58
+ */
59
+ export declare const storedWhole: (database: CacheDatabase, emailId: number) => boolean;
39
60
  export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
40
61
  export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
@@ -1,4 +1,5 @@
1
1
  import { fromJson } from "./events.js";
2
+ import { write } from "./transaction.js";
2
3
  export const locate = (database, accountId, emailId, where) => database
3
4
  .prepare(`INSERT OR REPLACE INTO email_locations (mailbox_id, uidvalidity, uid, email_id)
4
5
  SELECT id, ?, ?, ? FROM mailboxes WHERE account_id = ? AND external_id = ?`)
@@ -26,14 +27,22 @@ export const partialEmails = (database, accountId, limit) => {
26
27
  const rows = database
27
28
  .prepare(`SELECT e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.metadata,
28
29
  count(*) OVER () AS total,
29
- (SELECT json_group_array(json_object('folder', b.external_id, 'uidvalidity', l.uidvalidity, 'uid', l.uid))
30
- FROM email_locations l JOIN mailboxes b ON b.id = l.mailbox_id WHERE l.email_id = e.id) AS places
30
+ (SELECT json_group_array(json_object('folder', folder, 'uidvalidity', uidvalidity, 'uid', uid))
31
+ FROM (SELECT b.external_id AS folder, l.uidvalidity, l.uid
32
+ FROM email_locations l JOIN mailboxes b ON b.id = l.mailbox_id
33
+ WHERE l.email_id = e.id ORDER BY l.rowid DESC)) AS places
31
34
  FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
32
35
  WHERE e.account_id = ? AND e.deleted_at IS NULL AND json_extract(e.metadata, '$.partial') IS NOT NULL
36
+ AND json_extract(e.metadata, '$.partial.stuck') IS NULL
33
37
  ORDER BY at DESC, e.id DESC LIMIT ?`)
34
38
  .all(accountId, limit);
39
+ const stuck = database
40
+ .prepare(`SELECT count(*) AS n FROM emails WHERE account_id = ? AND deleted_at IS NULL
41
+ AND json_extract(metadata, '$.partial.stuck') IS NOT NULL`)
42
+ .get(accountId);
35
43
  return {
36
44
  total: rows.length ? Number(rows[0]?.total) : 0,
45
+ stuck: Number(stuck?.n ?? 0),
37
46
  emails: rows.map((row) => ({
38
47
  externalId: String(row.external_id),
39
48
  threadExternalId: String(row.thread),
@@ -47,6 +56,24 @@ export const partialEmails = (database, accountId, limit) => {
47
56
  })),
48
57
  };
49
58
  };
59
+ /** Replaces a partial email's `metadata.partial`, as a failed read records itself; a whole email is left alone. */
60
+ export const markPartial = (database, accountId, externalId, mark) => database
61
+ .prepare(`UPDATE emails SET metadata = json_set(metadata, '$.partial', json(?)) WHERE account_id = ? AND external_id = ?
62
+ AND json_extract(metadata, '$.partial') IS NOT NULL`)
63
+ .run(JSON.stringify(mark), accountId, externalId);
64
+ export const partialMail = (database) => ({
65
+ partialEmails: async (accountId, limit) => partialEmails(database, accountId, limit),
66
+ markPartial: async (accountId, externalId, mark) => {
67
+ write(database, () => markPartial(database, accountId, externalId, mark));
68
+ },
69
+ });
70
+ /**
71
+ * A live email stored whole, which a partial copy must not replace: a server search checks what is held before its
72
+ * slow read, and a sync in another process may store the email whole in between.
73
+ */
74
+ export const storedWhole = (database, emailId) => database
75
+ .prepare("SELECT 1 FROM emails WHERE id = ? AND deleted_at IS NULL AND json_extract(metadata, '$.partial') IS NULL")
76
+ .get(emailId) !== undefined;
50
77
  export const setLocations = (database, accountId, folder, uidvalidity, found) => {
51
78
  database
52
79
  .prepare(`DELETE FROM email_locations WHERE uidvalidity <> ?
@@ -3,13 +3,13 @@ import type { Attachment, Metadata } from "../../meetings/index.js";
3
3
  import type { Stemmer, Stemmers } from "../../search/stem.js";
4
4
  import type { CacheDatabase } from "../driver.js";
5
5
  import type { ConversationSummary, StoredHit } from "../types.js";
6
- import { type EmailLocationInput, type FolderEmail, type PartialEmail } from "./email-locations.js";
6
+ import { type EmailLocationInput, type FolderEmail, type PartialMailStore } from "./email-locations.js";
7
7
  import { type MailRulesStore } from "./mail-rules.js";
8
8
  import { type AttachmentInput } from "./meetings.js";
9
9
  import type { StoreContext } from "./open.js";
10
10
  import { type SyncStateSave } from "./sync.js";
11
11
  import { type ThreadFilter } from "./thread-columns.js";
12
- export type { EmailLocationInput, FolderEmail, PartialEmail };
12
+ export type { EmailLocationInput, FolderEmail, PartialEmail } from "./email-locations.js";
13
13
  export interface EmailAddress {
14
14
  address: string;
15
15
  name: string | null;
@@ -205,7 +205,7 @@ export interface SenderScore {
205
205
  demoted?: boolean;
206
206
  }
207
207
  /** Mail in tables of its own: threads, emails, recipients and mailboxes, keyed by account and Message-ID. */
208
- export interface MailStore {
208
+ export interface MailStore extends PartialMailStore {
209
209
  saveThread(input: ThreadSave): Promise<ThreadDetails>;
210
210
  /** Several threads and sync state in one transaction: a slice of mail commits with its resume point, or not at all. */
211
211
  saveThreads(inputs: ThreadSave[], states?: SyncStateSave[]): Promise<ThreadDetails[]>;
@@ -217,10 +217,6 @@ export interface MailStore {
217
217
  emails(filter?: EmailFilter): Promise<Email[]>;
218
218
  /** Each matching email's Message-ID and mailboxes, without text or people: cheap over a whole mailbox. */
219
219
  emailRefs(filter?: Omit<EmailFilter, "limit" | "offset">): Promise<EmailRef[]>;
220
- partialEmails(accountId: number, limit: number): Promise<{
221
- emails: PartialEmail[];
222
- total: number;
223
- }>;
224
220
  /** Each matching email's sender, recipients, list, size, flag and mailboxes, oldest first; no text. */
225
221
  emailFacts(filter?: Omit<EmailFilter, "limit" | "offset">): Promise<EmailFacts[]>;
226
222
  /**
@@ -2,7 +2,7 @@ import { formatLocator } from "../../domain/locator.js";
2
2
  import { formatReference } from "../../domain/references.js";
3
3
  import { fold, normalize } from "../normalize.js";
4
4
  import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
5
- import { folderEmails, forgetLocations, locate, partialEmails, setLocations, } from "./email-locations.js";
5
+ import { folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
6
6
  import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
7
7
  import { bool, flag, fromJson, int, page, str, toJson } from "./events.js";
8
8
  import { folded } from "./folded.js";
@@ -204,6 +204,8 @@ const saveEmail = (database, orm, accountId, threadId, mail, now) => {
204
204
  const found = database
205
205
  .prepare("SELECT id, email_thread_id FROM emails WHERE account_id = ? AND external_id = ?")
206
206
  .get(accountId, mail.externalId);
207
+ if (found && mail.metadata?.partial !== undefined && storedWhole(database, Number(found.id)))
208
+ return null;
207
209
  let emailId;
208
210
  if (found) {
209
211
  emailId = Number(found.id);
@@ -469,7 +471,7 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
469
471
  mailboxes: JSON.parse(String(row.boxes)),
470
472
  }));
471
473
  },
472
- partialEmails: async (accountId, limit) => partialEmails(database, accountId, limit),
474
+ ...partialMail(database),
473
475
  async emailFacts(filter = {}) {
474
476
  const [where, params] = emailWhere(filter);
475
477
  return database
@@ -13,6 +13,8 @@ export interface EvidenceSource {
13
13
  /** Deleted at its source since it was cited, or no longer in the store. */
14
14
  missing: boolean;
15
15
  }
16
+ /** Each distinct reference read back, in order; one the store no longer resolves keeps its reference, `missing`. */
17
+ export declare const sourcesOfReferences: (database: CacheDatabase, references: readonly string[]) => EvidenceSource[];
16
18
  /**
17
19
  * Every `evidence` link from a memory or fact, in the order they were written, including one whose target was
18
20
  * deleted since: `evidenceOf` drops those, so a reader would never learn the record lost a source.
@@ -21,5 +23,3 @@ export declare const evidenceSourcesOf: (database: CacheDatabase, from: {
21
23
  type: string;
22
24
  id: number;
23
25
  }) => EvidenceSource[];
24
- /** Each distinct reference read back, in order; one the store no longer resolves keeps its reference, `missing`. */
25
- export declare const sourcesOfReferences: (database: CacheDatabase, references: readonly string[]) => EvidenceSource[];
@@ -1,83 +1,47 @@
1
1
  import { linkKind } from "./link-kinds.js";
2
- import { referenceOfThing, stateOfThing, storedThing, thingOf } from "./things.js";
3
- const EXCERPT_CHARS = 280;
4
- const excerptOf = (text) => {
5
- if (text == null)
6
- return null;
7
- const flat = String(text).replace(/\s+/g, " ").trim();
8
- if (!flat)
9
- return null;
10
- return flat.length > EXCERPT_CHARS ? `${flat.slice(0, EXCERPT_CHARS - 1)}…` : flat;
11
- };
12
- const iso = (value) => (value == null ? null : new Date(Number(value)).toISOString());
13
- const textOf = (value) => (value == null || String(value).trim() === "" ? null : String(value));
14
- const READS = {
15
- message: "SELECT c.title, coalesce(m.sender_name, i.name) AS sender, m.sent_at AS at, m.text AS body, a.provider AS tool " +
16
- "FROM messages m JOIN chats c ON c.id = m.chat_id JOIN accounts a ON a.id = c.account_id " +
17
- "LEFT JOIN identities i ON i.id = m.sender_identity_id WHERE m.id = ?",
18
- email: "SELECT e.subject AS title, coalesce(e.from_name, e.from_address) AS sender, e.sent_at AS at, " +
19
- "coalesce(e.body_text, e.snippet) AS body, 'email' AS tool FROM emails e WHERE e.id = ?",
20
- note: "SELECT title, NULL AS sender, updated_at AS at, body, 'notes' AS tool FROM notes WHERE id = ?",
21
- document: "SELECT d.title, NULL AS sender, coalesce(d.external_updated_at, d.updated_at) AS at, d.body, a.provider AS tool " +
22
- "FROM documents d JOIN accounts a ON a.id = d.account_id WHERE d.id = ?",
23
- meeting: "SELECT m.title, NULL AS sender, m.started_at AS at, m.description AS body, a.provider AS tool " +
24
- "FROM meetings m JOIN accounts a ON a.id = m.account_id WHERE m.id = ?",
25
- meeting_transcript: "SELECT m.title, NULL AS sender, m.started_at AS at, NULL AS body, a.provider AS tool FROM meeting_transcripts t " +
26
- "JOIN meetings m ON m.id = t.meeting_id JOIN accounts a ON a.id = m.account_id WHERE t.id = ?",
27
- meeting_transcript_row: "SELECT m.title, r.speaker_name AS sender, m.started_at + coalesce(r.start_ms, 0) AS at, r.text AS body, " +
28
- "a.provider AS tool FROM meeting_transcript_rows r JOIN meeting_transcripts t ON t.id = r.meeting_transcript_id " +
29
- "JOIN meetings m ON m.id = t.meeting_id JOIN accounts a ON a.id = m.account_id WHERE r.id = ?",
30
- };
31
- const KIND = {
32
- meeting_transcript: "meeting",
33
- meeting_transcript_row: "meeting",
34
- };
35
- const sourceOf = (database, thing) => {
36
- const state = stateOfThing(database, thing);
37
- const read = READS[thing.type];
38
- const row = read === undefined || state === "unavailable" ? undefined : database.prepare(read).get(thing.id);
39
- const gone = state !== "available";
40
- return {
41
- ref: referenceOfThing(database, thing) ?? null,
42
- kind: KIND[thing.type] ?? thing.type,
43
- title: textOf(row?.title),
44
- sender: textOf(row?.sender),
45
- at: iso(row?.at),
46
- excerpt: gone ? null : excerptOf(row?.body),
47
- tool: textOf(row?.tool),
48
- missing: gone,
49
- };
50
- };
2
+ import { refReadsOf } from "./ref-summaries.js";
3
+ import { referenceOfThing, storedThing } from "./things.js";
4
+ /** A message is known by its chat here, not by the sender and time its summary title says. */
5
+ const sourceOf = (read, ref) => ({
6
+ ref,
7
+ kind: read.kind,
8
+ title: read.kind === "message" ? read.in : read.title,
9
+ sender: read.sender,
10
+ at: read.at,
11
+ excerpt: read.excerpt,
12
+ tool: read.provider,
13
+ missing: read.state !== "available",
14
+ });
15
+ /** Each distinct reference read back, in order; one the store no longer resolves keeps its reference, `missing`. */
16
+ export const sourcesOfReferences = (database, references) => [...refReadsOf(database, references, { excerpt: true })].map(([ref, read]) => sourceOf(read, ref));
17
+ const GONE_KIND = { meeting_transcript: "meeting", meeting_transcript_row: "meeting" };
51
18
  /**
52
19
  * Every `evidence` link from a memory or fact, in the order they were written, including one whose target was
53
20
  * deleted since: `evidenceOf` drops those, so a reader would never learn the record lost a source.
54
21
  */
55
- export const evidenceSourcesOf = (database, from) => database
56
- .prepare(`SELECT to_type, to_id FROM links WHERE from_type = ? AND from_id = ? AND kind = ${linkKind("evidence")} ` +
57
- "AND to_id IS NOT NULL ORDER BY id")
58
- .all(from.type, from.id)
59
- .map((link) => sourceOf(database, storedThing(link.to_type, link.to_id)));
60
- const thingNamed = (database, reference) => {
61
- try {
62
- return thingOf(database, reference);
63
- }
64
- catch {
65
- return undefined;
66
- }
22
+ export const evidenceSourcesOf = (database, from) => {
23
+ const cited = database
24
+ .prepare(`SELECT to_type, to_id FROM links WHERE from_type = ? AND from_id = ? AND kind = ${linkKind("evidence")} ` +
25
+ "AND to_id IS NOT NULL ORDER BY id")
26
+ .all(from.type, from.id)
27
+ .map((link) => {
28
+ const thing = storedThing(link.to_type, link.to_id);
29
+ return { type: thing.type, ref: referenceOfThing(database, thing) };
30
+ });
31
+ const reads = refReadsOf(database, cited.flatMap(({ ref }) => ref ?? []), { excerpt: true });
32
+ return cited.map(({ type, ref }) => {
33
+ const read = ref === undefined ? undefined : reads.get(ref);
34
+ if (ref !== undefined && read !== undefined)
35
+ return sourceOf(read, ref);
36
+ return {
37
+ ref: null,
38
+ kind: GONE_KIND[type] ?? type,
39
+ title: null,
40
+ sender: null,
41
+ at: null,
42
+ excerpt: null,
43
+ tool: null,
44
+ missing: true,
45
+ };
46
+ });
67
47
  };
68
- /** Each distinct reference read back, in order; one the store no longer resolves keeps its reference, `missing`. */
69
- export const sourcesOfReferences = (database, references) => [...new Set(references)].map((reference) => {
70
- const thing = thingNamed(database, reference);
71
- if (thing)
72
- return { ...sourceOf(database, thing), ref: reference };
73
- return {
74
- ref: reference,
75
- kind: reference.startsWith("msg:") ? "message" : reference.split(":")[0] || "unknown",
76
- title: null,
77
- sender: null,
78
- at: null,
79
- excerpt: null,
80
- tool: null,
81
- missing: true,
82
- };
83
- });
@@ -1,17 +1,17 @@
1
1
  import { type QueryAst } from "@wirecat/cli-base/query";
2
2
  import { type Decider } from "../decisions.js";
3
3
  import type { AccountKey } from "../types.js";
4
- import { RELATION_KINDS } from "./link-kinds.js";
5
4
  import { type MeetingProjectsStore } from "./meeting-projects.js";
6
5
  import type { Author } from "./memories.js";
7
6
  import { type Link } from "./notes.js";
8
7
  import type { StoreContext } from "./open.js";
9
8
  import type { ProposedAction } from "./proposed-actions.js";
10
- import { type LinkDecision } from "./relation-proposals.js";
9
+ import { type KnowledgeRelation, type LinkDecision, type RelateInput, type RelationUpdate } from "./relation-proposals.js";
11
10
  import type { TagCreationOptions } from "./tag-creation.js";
12
11
  import { type Label, type TagKind, type TagRow } from "./tags.js";
13
12
  import { type ThingState } from "./things.js";
14
13
  import { type VocabularyStore } from "./vocabulary.js";
14
+ export type { KnowledgeRelation, RelateInput } from "./relation-proposals.js";
15
15
  export type { Label } from "./tags.js";
16
16
  export type KnowledgeTarget = {
17
17
  type: "message";
@@ -90,18 +90,6 @@ export interface Project {
90
90
  scope: (typeof SCOPES)[number];
91
91
  createdAt: string;
92
92
  }
93
- export interface KnowledgeRelation {
94
- id: string;
95
- from: string;
96
- to: string;
97
- kind: (typeof RELATION_KINDS)[number];
98
- role: string | null;
99
- evidence: string | null;
100
- confirmed: boolean;
101
- provenance: string | null;
102
- confidenceCategory: "owner-confirmed" | "weak";
103
- createdAt: string;
104
- }
105
93
  export interface Reminder {
106
94
  id: string;
107
95
  task: string;
@@ -199,9 +187,24 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
199
187
  to: string;
200
188
  kind: KnowledgeRelation["kind"];
201
189
  role?: string;
190
+ purpose?: string;
202
191
  evidence?: string[];
203
192
  reason?: string;
204
193
  }, by: Exclude<Author, "owner">): Promise<ProposedAction>;
194
+ /** The owner sets a relationship's role and purpose; `null` clears one. */
195
+ updateRelation(key: KnowledgeScope, id: string, input: RelationUpdate & {
196
+ by: "owner";
197
+ }): Promise<KnowledgeRelation>;
198
+ /** An agent's change is a `relationship_update` proposal for the owner; the same pending change is answered again. */
199
+ updateRelation(key: KnowledgeScope, id: string, input: RelationUpdate & {
200
+ by: Exclude<Author, "owner">;
201
+ reason?: string;
202
+ }): Promise<ProposedAction>;
203
+ /** Decides a `relationship_update` proposal; a confirmed one changes the relationship in the same transaction. */
204
+ decideRelationUpdate(id: string, decision: LinkDecision): Promise<{
205
+ proposal: ProposedAction;
206
+ relation: KnowledgeRelation | null;
207
+ }>;
205
208
  /** Decides a stored `relationship` or `key_document` proposal; the link and the decision land together. */
206
209
  decideLinkProposal(id: string, decision: LinkDecision): Promise<{
207
210
  proposal: ProposedAction;
@@ -229,13 +232,4 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
229
232
  }): Promise<Reminder[]>;
230
233
  acknowledgeReminder(key: AccountKey, id: string, receipt: string): Promise<Reminder>;
231
234
  }
232
- export interface RelateInput {
233
- from: string;
234
- to: string;
235
- kind: KnowledgeRelation["kind"];
236
- role?: string;
237
- evidence?: string;
238
- confirmed?: boolean;
239
- provenance?: string;
240
- }
241
235
  export declare const knowledgeStoreOver: (context: StoreContext) => KnowledgeStore;