@wirecat/cli-core 0.546.0 → 0.547.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 +152 -0
  2. package/dist/cli/messenger/history-query.d.ts +10 -4
  3. package/dist/cli/messenger/history-query.js +87 -31
  4. package/dist/cli/messenger/messages-command.js +10 -4
  5. package/dist/cli/messenger/messages-comments-command.js +4 -2
  6. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  7. package/dist/cli/option-hints.d.ts +6 -0
  8. package/dist/cli/option-hints.js +15 -0
  9. package/dist/errors/reasons.js +13 -1
  10. package/dist/mail/cli/index.js +4 -4
  11. package/dist/mail/index.d.ts +2 -0
  12. package/dist/mail/index.js +2 -0
  13. package/dist/mail/partial.d.ts +34 -0
  14. package/dist/mail/partial.js +94 -0
  15. package/dist/mail/server-search.js +17 -9
  16. package/dist/mail/source.d.ts +13 -2
  17. package/dist/mail/source.js +7 -1
  18. package/dist/mail/sync-types.d.ts +112 -0
  19. package/dist/mail/sync-types.js +1 -0
  20. package/dist/mail/sync.d.ts +4 -106
  21. package/dist/mail/sync.js +49 -4
  22. package/dist/mail/testing/fake.d.ts +3 -1
  23. package/dist/mail/testing/fake.js +10 -2
  24. package/dist/services/deps.d.ts +0 -2
  25. package/dist/services/deps.js +0 -2
  26. package/dist/services/documents/ingestion.js +1 -1
  27. package/dist/services/documents/knowledge.d.ts +10 -6
  28. package/dist/services/documents/knowledge.js +25 -11
  29. package/dist/services/index.d.ts +1 -0
  30. package/dist/services/index.js +1 -0
  31. package/dist/services/messages.d.ts +2 -0
  32. package/dist/services/messages.js +25 -23
  33. package/dist/services/proposal-tasks.d.ts +3 -2
  34. package/dist/services/proposal-tasks.js +5 -2
  35. package/dist/services/proposals/ids.d.ts +5 -0
  36. package/dist/services/proposals/ids.js +43 -0
  37. package/dist/services/proposals/index.d.ts +3 -0
  38. package/dist/services/proposals/index.js +2 -0
  39. package/dist/services/proposals/kinds/action.d.ts +7 -0
  40. package/dist/services/proposals/kinds/action.js +45 -0
  41. package/dist/services/proposals/kinds/fact.d.ts +4 -0
  42. package/dist/services/proposals/kinds/fact.js +43 -0
  43. package/dist/services/proposals/kinds/findings.d.ts +7 -0
  44. package/dist/services/proposals/kinds/findings.js +75 -0
  45. package/dist/services/proposals/kinds/key-document.d.ts +9 -0
  46. package/dist/services/proposals/kinds/key-document.js +75 -0
  47. package/dist/services/proposals/kinds/memory.d.ts +4 -0
  48. package/dist/services/proposals/kinds/memory.js +42 -0
  49. package/dist/services/proposals/kinds/relationship.d.ts +10 -0
  50. package/dist/services/proposals/kinds/relationship.js +82 -0
  51. package/dist/services/proposals/kinds/shared.d.ts +25 -0
  52. package/dist/services/proposals/kinds/shared.js +49 -0
  53. package/dist/services/proposals/registry.d.ts +9 -0
  54. package/dist/services/proposals/registry.js +129 -0
  55. package/dist/services/proposals/types.d.ts +85 -0
  56. package/dist/services/proposals/types.js +1 -0
  57. package/dist/services/server-search.d.ts +7 -4
  58. package/dist/services/server-search.js +5 -2
  59. package/dist/services/tasks.js +8 -4
  60. package/dist/store/decisions.d.ts +65 -0
  61. package/dist/store/decisions.js +55 -0
  62. package/dist/store/evidence-findings.d.ts +6 -6
  63. package/dist/store/index.d.ts +4 -3
  64. package/dist/store/index.js +2 -1
  65. package/dist/store/meeting-findings.d.ts +2 -0
  66. package/dist/store/messages.d.ts +4 -9
  67. package/dist/store/sqlite/actors.d.ts +13 -2
  68. package/dist/store/sqlite/actors.js +24 -1
  69. package/dist/store/sqlite/email-locations.d.ts +16 -0
  70. package/dist/store/sqlite/email-locations.js +26 -0
  71. package/dist/store/sqlite/emails.d.ts +6 -2
  72. package/dist/store/sqlite/emails.js +2 -1
  73. package/dist/store/sqlite/evidence-findings.js +13 -3
  74. package/dist/store/sqlite/facts.d.ts +16 -8
  75. package/dist/store/sqlite/facts.js +48 -15
  76. package/dist/store/sqlite/findings-core.d.ts +2 -1
  77. package/dist/store/sqlite/findings-core.js +4 -4
  78. package/dist/store/sqlite/knowledge.d.ts +43 -7
  79. package/dist/store/sqlite/knowledge.js +55 -15
  80. package/dist/store/sqlite/meeting-findings.js +6 -6
  81. package/dist/store/sqlite/memories.d.ts +12 -8
  82. package/dist/store/sqlite/memories.js +33 -10
  83. package/dist/store/sqlite/notes.d.ts +5 -1
  84. package/dist/store/sqlite/notes.js +18 -2
  85. package/dist/store/sqlite/open-store.d.ts +4 -1
  86. package/dist/store/sqlite/open-store.js +8 -2
  87. package/dist/store/sqlite/open.d.ts +3 -0
  88. package/dist/store/sqlite/proposed-actions.d.ts +21 -8
  89. package/dist/store/sqlite/proposed-actions.js +29 -9
  90. package/dist/store/sqlite/reads.d.ts +13 -2
  91. package/dist/store/sqlite/reads.js +8 -4
  92. package/dist/store/sqlite/relation-proposals.d.ts +34 -0
  93. package/dist/store/sqlite/relation-proposals.js +114 -0
  94. package/dist/store/store.d.ts +3 -0
  95. package/dist/store/store.js +2 -0
  96. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ /**
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.
4
+ */
5
+ export type Proposer = {
6
+ type: "person";
7
+ id: string;
8
+ } | {
9
+ type: "bot";
10
+ id: string;
11
+ name: string;
12
+ kind: "agent" | "rule";
13
+ } | {
14
+ type: "unknown";
15
+ };
16
+ /** Who decides: the owner, or an agent the host lets decide for the owner, with its reason. */
17
+ export type Decider = "owner" | {
18
+ bot: string;
19
+ model?: string;
20
+ reason: string;
21
+ };
22
+ export interface DecisionPolicy {
23
+ /** The host's `agent_decisions`: `false` keeps every decision with the owner. */
24
+ agentDecisions: boolean;
25
+ /** A rule scorer's proposal is the owner's alone to decide. */
26
+ ruleProposalsOwnerOnly: boolean;
27
+ }
28
+ export declare const DEFAULT_DECISION_POLICY: DecisionPolicy;
29
+ export interface Refusal {
30
+ id: string;
31
+ code: "permission_error" | "validation_error" | "not_found";
32
+ reason: string;
33
+ message: string;
34
+ }
35
+ /**
36
+ * The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
37
+ * Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
38
+ */
39
+ export declare const mayDecide: (row: {
40
+ id: string;
41
+ proposedBy: Proposer;
42
+ }, by: Decider, policy?: DecisionPolicy) => Refusal | null;
43
+ /** One bot whatever its letter case or spacing: `Claude-Code` is `claude-code`. */
44
+ export declare const botKey: (name: string) => string;
45
+ /**
46
+ * Removing or reversing a row: the owner always may; an agent only withdraws its own row while it is still
47
+ * proposed. Everything else stays the owner's.
48
+ */
49
+ export declare const assertMayWithdraw: (row: {
50
+ id: string;
51
+ proposedBy: Proposer;
52
+ proposed: boolean;
53
+ }, by: Decider | Exclude<Decider, "owner"> | {
54
+ bot: string;
55
+ }) => void;
56
+ /** `mayDecide` for a store method: throws the refusal instead of returning it. */
57
+ export declare const assertMayDecide: (row: {
58
+ id: string;
59
+ proposedBy: Proposer;
60
+ }, by: Decider, policy?: DecisionPolicy) => void;
61
+ /** The author a decision is recorded under. */
62
+ export declare const deciderAuthor: (by: Decider) => "owner" | {
63
+ bot: string;
64
+ model?: string;
65
+ };
@@ -0,0 +1,55 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ export const DEFAULT_DECISION_POLICY = { agentDecisions: true, ruleProposalsOwnerOnly: true };
3
+ /**
4
+ * The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
5
+ * Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
6
+ */
7
+ export const mayDecide = (row, by, policy = DEFAULT_DECISION_POLICY) => {
8
+ if (by === "owner")
9
+ return null;
10
+ const refuse = (code, reason, message) => ({
11
+ id: row.id,
12
+ code,
13
+ reason,
14
+ message,
15
+ });
16
+ if (!policy.agentDecisions)
17
+ return refuse("permission_error", "proposals.agent-decisions-off", "agent decisions are turned off; the owner decides");
18
+ if (typeof by.reason !== "string" || !by.reason.trim() || by.reason.length > 2000)
19
+ return refuse("validation_error", "proposal.reason-required", "an agent deciding for the owner gives a reason");
20
+ const proposer = row.proposedBy;
21
+ if (proposer.type === "unknown")
22
+ return refuse("permission_error", "proposal.owner-only", "this proposal names no author; only the owner decides it");
23
+ if (proposer.type === "bot" && proposer.kind === "rule" && policy.ruleProposalsOwnerOnly)
24
+ return refuse("permission_error", "proposal.owner-only", `proposed by ${proposer.name}: only the owner decides it`);
25
+ if (proposer.type === "bot" && proposer.kind === "agent" && botKey(proposer.name) === botKey(by.bot))
26
+ return refuse("permission_error", "proposal.own", `${by.bot} proposed this; another decider must decide it`);
27
+ return null;
28
+ };
29
+ /** One bot whatever its letter case or spacing: `Claude-Code` is `claude-code`. */
30
+ export const botKey = (name) => name
31
+ .trim()
32
+ .toLowerCase()
33
+ .replace(/[\s_]+/g, "-");
34
+ /**
35
+ * Removing or reversing a row: the owner always may; an agent only withdraws its own row while it is still
36
+ * proposed. Everything else stays the owner's.
37
+ */
38
+ export const assertMayWithdraw = (row, by) => {
39
+ if (by === "owner")
40
+ return;
41
+ const proposer = row.proposedBy;
42
+ if (row.proposed && proposer.type === "bot" && proposer.kind === "agent" && botKey(proposer.name) === botKey(by.bot))
43
+ return;
44
+ throw new CliError("permission_error", `only the owner removes or reverses ${row.id}`, {
45
+ reason: "proposal.owner-only",
46
+ });
47
+ };
48
+ /** `mayDecide` for a store method: throws the refusal instead of returning it. */
49
+ export const assertMayDecide = (row, by, policy = DEFAULT_DECISION_POLICY) => {
50
+ const refusal = mayDecide(row, by, policy);
51
+ if (refusal)
52
+ throw new CliError(refusal.code, refusal.message, { reason: refusal.reason });
53
+ };
54
+ /** The author a decision is recorded under. */
55
+ export const deciderAuthor = (by) => by === "owner" ? "owner" : { bot: by.bot, ...(by.model === undefined ? {} : { model: by.model }) };
@@ -1,16 +1,14 @@
1
1
  import { type FindingOutcome, type FindingsInput, type FindingsReceipt } from "../meetings/index.js";
2
+ import type { Decider } from "./decisions.js";
2
3
  import type { EvidenceSource } from "./sqlite/evidence-sources.js";
3
4
  export declare const EVIDENCE_FINDINGS_KIND = "evidence_findings";
4
5
  export declare const validateEvidenceFindings: (input: FindingsInput) => FindingsInput;
5
6
  /** Who decided: the owner, or an agent the host let decide for the owner, with its reason. */
6
- export type EvidenceFindingsDecider = "owner" | {
7
- bot: string;
8
- model?: string;
9
- reason: string;
10
- };
7
+ export type EvidenceFindingsDecider = Decider;
11
8
  export interface EvidenceFindingsReceipt extends FindingsReceipt {
12
9
  decision: {
13
10
  by: "owner";
11
+ reason?: string;
14
12
  } | {
15
13
  by: "agent";
16
14
  bot: string;
@@ -31,6 +29,7 @@ export interface EvidenceFindingsBatch {
31
29
  type: "person" | "bot";
32
30
  id: string;
33
31
  name: string | null;
32
+ kind: "agent" | "rule" | null;
34
33
  };
35
34
  decidedBy: {
36
35
  type: "person" | "bot";
@@ -62,7 +61,8 @@ export interface EvidenceFindingsStore {
62
61
  review(id: string, input: {
63
62
  fingerprint: string;
64
63
  accept: string[];
65
- by?: EvidenceFindingsDecider;
64
+ by: EvidenceFindingsDecider;
65
+ reason?: string;
66
66
  }): Promise<EvidenceFindingsBatch>;
67
67
  /** Each reference the batch cites, read back once; one gone from the store since is `missing`. */
68
68
  sources(id: string): Promise<EvidenceSource[]>;
@@ -7,6 +7,7 @@ export { note } from "../entities/note.js";
7
7
  export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFinding, MeetingFindingsInput, MeetingFindingsReceipt, MeetingTaskFinding, } from "../meetings/index.js";
8
8
  export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, type PriorityLabel, priorityLabel, priorityOf, priorityOption, TASK_STATES, type TaskState, } from "../tasks/index.js";
9
9
  export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
10
+ export { assertMayDecide, DEFAULT_DECISION_POLICY, type Decider, type DecisionPolicy, mayDecide, type Proposer, type Refusal, } from "./decisions.js";
10
11
  export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransferReceipt, } from "./document-transfer/index.js";
11
12
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
12
13
  export type { CacheDatabase, CacheStatement, OpenDatabase, SqlValue } from "./driver.js";
@@ -30,7 +31,7 @@ export type { BotUpdate, BotUpdateStore } from "./sqlite/bot-updates.js";
30
31
  export type { CatalogActor, CatalogEntry, CatalogInput, CatalogQuery, CatalogStore } from "./sqlite/catalog.js";
31
32
  export { CATALOG_QUERY } from "./sqlite/catalog.js";
32
33
  export type { ChatMetadata } from "./sqlite/chats.js";
33
- export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
34
+ export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
34
35
  export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
35
36
  export type { EvidenceSource } from "./sqlite/evidence-sources.js";
36
37
  export type { Fact, FactInput, FactsStore } from "./sqlite/facts.js";
@@ -49,8 +50,8 @@ export type { NoteIndexState } from "./sqlite/note-index.js";
49
50
  export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, type NoteQuery, type NoteSearch, } from "./sqlite/note-search.js";
50
51
  export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
51
52
  export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
52
- export type { ProposalInput, ProposedAction, ProposedActionsStore } from "./sqlite/proposed-actions.js";
53
- export { PROPOSAL_STATUSES } from "./sqlite/proposed-actions.js";
53
+ export { PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
54
+ export type { LinkDecision } from "./sqlite/relation-proposals.js";
54
55
  export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchIndexState, searchIndexState, } from "./sqlite/search-index.js";
55
56
  export type { SyncStateSave } from "./sqlite/sync.js";
56
57
  export type { TagCreationOptions, TagCreationReview } from "./sqlite/tag-creation.js";
@@ -2,6 +2,7 @@ export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES
2
2
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
3
3
  export { note } from "../entities/note.js";
4
4
  export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, priorityLabel, priorityOf, priorityOption, TASK_STATES, } from "../tasks/index.js";
5
+ export { assertMayDecide, DEFAULT_DECISION_POLICY, mayDecide, } from "./decisions.js";
5
6
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
6
7
  export { PRAGMAS } from "./driver.js";
7
8
  export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
@@ -21,7 +22,7 @@ export { EVENT_FIELDS, } from "./sqlite/event-search.js";
21
22
  export { IMPORTED_DOCUMENT_QUERY } from "./sqlite/imported-documents.js";
22
23
  export { ORGANIZATION_KINDS, PROJECT_TYPES, SCOPES } from "./sqlite/knowledge.js";
23
24
  export { NOTE_FIELDS, } from "./sqlite/note-search.js";
24
- export { PROPOSAL_STATUSES } from "./sqlite/proposed-actions.js";
25
+ export { PROPOSAL_STATUSES, proposerOfAction, } from "./sqlite/proposed-actions.js";
25
26
  export { fillSearchIndex, resetSearchIndex, searchIndexState, } from "./sqlite/search-index.js";
26
27
  export { PROJECT_TASK_FIELDS, TASK_SORTS } from "./sqlite/task-board.js";
27
28
  export { certaintyOf } from "./sqlite/task-close.js";
@@ -1,4 +1,5 @@
1
1
  import type { FindingOutcome, MeetingFindingsInput, MeetingFindingsReceipt } from "../meetings/index.js";
2
+ import type { Decider } from "./decisions.js";
2
3
  import type { AccountKey } from "./types.js";
3
4
  export interface MeetingFindingsBatch {
4
5
  id: string;
@@ -36,5 +37,6 @@ export interface MeetingFindingsStore {
36
37
  review(account: AccountKey, id: string, input: {
37
38
  fingerprint: string;
38
39
  accept: string[];
40
+ by: Decider;
39
41
  }): Promise<MeetingFindingsBatch>;
40
42
  }
@@ -2,6 +2,7 @@ import type { CounterObservations } from "../domain/counters.js";
2
2
  import type { Delivery, Id, Message, Page, PersonReaction, PollVote, Reactions, Reactor, WindowedMessage } from "../domain/models.js";
3
3
  import { type DocumentLinkMessage } from "./sqlite/document-links.js";
4
4
  import type { StoreContext } from "./sqlite/open.js";
5
+ import * as reads from "./sqlite/reads.js";
5
6
  import type { AccountKey, DeletionScope, IngestedVia, MessageFilter, SenderChatStats, StoredHit } from "./types.js";
6
7
  export interface AccountMessages {
7
8
  /**
@@ -40,16 +41,10 @@ export interface AccountMessages {
40
41
  /** Oldest to newest, like a provider's history page. */
41
42
  /**
42
43
  * `threadId` keeps one forum topic's; `outgoing` only the owner's (true) or only the others' (false); `starred`
43
- * only the ones the owner starred (true) or did not (false).
44
+ * only the ones the owner starred (true) or did not (false). `after` reads forward instead of back, and `until`
45
+ * ends the window; `reads.MessagePageWindow` says how.
44
46
  */
45
- messages(key: AccountKey, chatId: Id, window: {
46
- limit: number;
47
- before?: Id;
48
- since?: string;
49
- threadId?: Id;
50
- outgoing?: boolean;
51
- starred?: boolean;
52
- }): Promise<Page<Message>>;
47
+ messages(key: AccountKey, chatId: Id, window: reads.MessagePageWindow): Promise<Page<Message>>;
53
48
  /** How many stored messages the chat has, sent at `since` or later when it is given. */
54
49
  countMessages(key: AccountKey, chatId: Id, options?: {
55
50
  since?: string;
@@ -1,3 +1,4 @@
1
+ import type { Proposer } from "../decisions.js";
1
2
  import type { CacheDatabase } from "../driver.js";
2
3
  /** Who made or owns something: the store's polymorphic actor pair. */
3
4
  export interface Actor {
@@ -6,7 +7,7 @@ export interface Actor {
6
7
  }
7
8
  /** How the task package names who acted; each maps to one stable actor row. */
8
9
  export type Origin = "owner" | "agent" | "rule";
9
- declare const BOT_KINDS: readonly ["agent", "script", "integration"];
10
+ export declare const BOT_KINDS: readonly ["agent", "rule", "script", "integration"];
10
11
  /** The owner's person row, which the initial migration seeds. */
11
12
  export declare const ownerPerson: (database: CacheDatabase) => Actor;
12
13
  /** A bot by its unique handle, registered on first use. */
@@ -17,4 +18,14 @@ export declare const botNamed: (database: CacheDatabase, name: string, now: numb
17
18
  */
18
19
  export declare const actorOfOrigin: (database: CacheDatabase, origin: Origin) => Actor;
19
20
  export declare const originOfActor: (database: CacheDatabase, type: unknown, id: unknown) => Origin | undefined;
20
- export {};
21
+ /** Who proposed a stored row, from its actor pair; the seeded bot `rule` and a bot of kind `rule` are rules. */
22
+ export declare const proposerOf: (database: CacheDatabase, type: unknown, id: unknown) => Proposer;
23
+ /** Who proposed something, as a list filter: any agent, any rule, or one bot by its handle. */
24
+ export type ProposedByFilter = "agent" | "rule" | {
25
+ bot: string;
26
+ };
27
+ /** The SQL that keeps rows whose actor pair matches `by`; the seeded bot `rule` counts as a rule. */
28
+ export declare const proposedBySql: (type: string, id: string, by: ProposedByFilter | undefined) => {
29
+ sql: string;
30
+ params: string[];
31
+ };
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- const BOT_KINDS = ["agent", "script", "integration"];
2
+ export const BOT_KINDS = ["agent", "rule", "script", "integration"];
3
3
  /** The owner's person row, which the initial migration seeds. */
4
4
  export const ownerPerson = (database) => {
5
5
  const found = database.prepare("SELECT id FROM persons WHERE owner = 1 ORDER BY id LIMIT 1").get();
@@ -41,3 +41,26 @@ export const originOfActor = (database, type, id) => {
41
41
  return "owner";
42
42
  return database.prepare("SELECT name FROM bots WHERE id = ?").get(Number(id))?.name === "rule" ? "rule" : "agent";
43
43
  };
44
+ /** Who proposed a stored row, from its actor pair; the seeded bot `rule` and a bot of kind `rule` are rules. */
45
+ export const proposerOf = (database, type, id) => {
46
+ if (type === "person" && id != null)
47
+ return { type: "person", id: String(id) };
48
+ if (type !== "bot" || id == null)
49
+ return { type: "unknown" };
50
+ const bot = database.prepare("SELECT name, kind FROM bots WHERE id = ?").get(Number(id));
51
+ if (!bot)
52
+ return { type: "unknown" };
53
+ const name = String(bot.name);
54
+ return { type: "bot", id: String(id), name, kind: bot.kind === "rule" || name === "rule" ? "rule" : "agent" };
55
+ };
56
+ /** The SQL that keeps rows whose actor pair matches `by`; the seeded bot `rule` counts as a rule. */
57
+ export const proposedBySql = (type, id, by) => {
58
+ if (by === undefined)
59
+ return { sql: "1", params: [] };
60
+ const bot = (condition) => `${type} = 'bot' AND EXISTS (SELECT 1 FROM bots pb WHERE pb.id = ${id} AND ${condition})`;
61
+ if (by === "rule")
62
+ return { sql: bot("(pb.kind = 'rule' OR pb.name = 'rule')"), params: [] };
63
+ if (by === "agent")
64
+ return { sql: bot("pb.kind <> 'rule' AND pb.name <> 'rule'"), params: [] };
65
+ return { sql: bot("pb.name = ?"), params: [by.bot] };
66
+ };
@@ -1,3 +1,4 @@
1
+ import type { Metadata } from "../../meetings/index.js";
1
2
  import type { CacheDatabase } from "../driver.js";
2
3
  /** Where an IMAP server holds an email: `folder` is the mailbox's external id. */
3
4
  export interface EmailLocationInput {
@@ -20,5 +21,20 @@ export declare const locate: (database: CacheDatabase, accountId: number, emailI
20
21
  changes: number;
21
22
  };
22
23
  export declare const folderEmails: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string) => FolderEmail[];
24
+ /**
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.
27
+ */
28
+ export interface PartialEmail {
29
+ externalId: string;
30
+ threadExternalId: string;
31
+ receivedAt: number | null;
32
+ metadata: Metadata;
33
+ locations: EmailLocationInput[];
34
+ }
35
+ export declare const partialEmails: (database: CacheDatabase, accountId: number, limit: number) => {
36
+ emails: PartialEmail[];
37
+ total: number;
38
+ };
23
39
  export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
24
40
  export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
@@ -1,3 +1,4 @@
1
+ import { fromJson } from "./events.js";
1
2
  export const locate = (database, accountId, emailId, where) => database
2
3
  .prepare(`INSERT OR REPLACE INTO email_locations (mailbox_id, uidvalidity, uid, email_id)
3
4
  SELECT id, ?, ?, ? FROM mailboxes WHERE account_id = ? AND external_id = ?`)
@@ -21,6 +22,31 @@ export const folderEmails = (database, accountId, folder, uidvalidity) => databa
21
22
  locations: Number(row.locations),
22
23
  folders: Number(row.folders),
23
24
  }));
25
+ export const partialEmails = (database, accountId, limit) => {
26
+ const rows = database
27
+ .prepare(`SELECT e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.metadata,
28
+ 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
31
+ FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
32
+ WHERE e.account_id = ? AND e.deleted_at IS NULL AND json_extract(e.metadata, '$.partial') IS NOT NULL
33
+ ORDER BY at DESC, e.id DESC LIMIT ?`)
34
+ .all(accountId, limit);
35
+ return {
36
+ total: rows.length ? Number(rows[0]?.total) : 0,
37
+ emails: rows.map((row) => ({
38
+ externalId: String(row.external_id),
39
+ threadExternalId: String(row.thread),
40
+ receivedAt: row.at === null ? null : Number(row.at),
41
+ metadata: fromJson(row.metadata),
42
+ locations: JSON.parse(String(row.places)).map((place) => ({
43
+ folder: String(place.folder),
44
+ uidvalidity: String(place.uidvalidity),
45
+ uid: Number(place.uid),
46
+ })),
47
+ })),
48
+ };
49
+ };
24
50
  export const setLocations = (database, accountId, folder, uidvalidity, found) => {
25
51
  database
26
52
  .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 } from "./email-locations.js";
6
+ import { type EmailLocationInput, type FolderEmail, type PartialEmail } 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 };
12
+ export type { EmailLocationInput, FolderEmail, PartialEmail };
13
13
  export interface EmailAddress {
14
14
  address: string;
15
15
  name: string | null;
@@ -217,6 +217,10 @@ 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
+ }>;
220
224
  /** Each matching email's sender, recipients, list, size, flag and mailboxes, oldest first; no text. */
221
225
  emailFacts(filter?: Omit<EmailFilter, "limit" | "offset">): Promise<EmailFacts[]>;
222
226
  /**
@@ -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, setLocations, } from "./email-locations.js";
5
+ import { folderEmails, forgetLocations, locate, partialEmails, setLocations, } 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";
@@ -469,6 +469,7 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
469
469
  mailboxes: JSON.parse(String(row.boxes)),
470
470
  }));
471
471
  },
472
+ partialEmails: async (accountId, limit) => partialEmails(database, accountId, limit),
472
473
  async emailFacts(filter = {}) {
473
474
  const [where, params] = emailWhere(filter);
474
475
  return database
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { contextEvidenceFingerprint } from "../../context/evidence.js";
3
3
  import { findingFingerprint, findingHash } from "../../meetings/index.js";
4
+ import { assertMayDecide } from "../decisions.js";
4
5
  import { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "../evidence-findings.js";
5
6
  import { contextRecordOf } from "./context-evidence.js";
6
7
  import { sourcesOfReferences } from "./evidence-sources.js";
@@ -80,8 +81,10 @@ export const evidenceFindingsStoreOver = (context) => {
80
81
  };
81
82
  const snapshot = (input) => core.snapshot(input, cite());
82
83
  const decider = (by) => {
83
- if (by === undefined || by === "owner")
84
+ if (by === "owner")
84
85
  return { author: "owner", decision: { by: "owner" } };
86
+ if (by === undefined)
87
+ return invalid("a findings review names who decides: the owner or an agent with a reason");
85
88
  const raw = by;
86
89
  const agent = findingsAgent({
87
90
  bot: raw.bot,
@@ -157,7 +160,14 @@ export const evidenceFindingsStoreOver = (context) => {
157
160
  decision.accept.some((candidate) => !batch.input.findings.some((finding) => finding.id === candidate)))
158
161
  return invalid("accept only unique IDs shown in this batch");
159
162
  const accepted = [...decision.accept].sort();
160
- const { author, decision: decidedBy } = decider(decision.by);
163
+ const { author, decision: checked } = decider(decision.by);
164
+ const reason = typeof decision.reason === "string" ? decision.reason.trim().slice(0, 2000) : "";
165
+ const decidedBy = reason && checked.by === "owner"
166
+ ? { ...checked, reason }
167
+ : reason && checked.by === "agent"
168
+ ? { ...checked, reason: `${checked.reason}: ${reason}` }
169
+ : checked;
170
+ assertMayDecide({ id, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
161
171
  if (batch.receipt) {
162
172
  if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
163
173
  return invalid("this findings batch was already reviewed with a different decision");
@@ -187,7 +197,7 @@ export const evidenceFindingsStoreOver = (context) => {
187
197
  bot: proposal.proposedBy.name,
188
198
  ...(input.agentModel === null ? {} : { model: input.agentModel }),
189
199
  };
190
- const items = core.outcomes(null, batch.input, accepted, agent);
200
+ const items = core.outcomes(null, batch.input, accepted, agent, decision.by);
191
201
  return view(proposals.approve(id, {
192
202
  previewFingerprint: batch.fingerprint,
193
203
  accepted,
@@ -1,5 +1,6 @@
1
1
  import { type FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
2
- import type { Actor } from "./actors.js";
2
+ import { type Decider } from "../decisions.js";
3
+ import { type Actor, type ProposedByFilter } from "./actors.js";
3
4
  import { type EvidenceSource } from "./evidence-sources.js";
4
5
  import { type Author } from "./memories.js";
5
6
  import type { StoreContext } from "./open.js";
@@ -18,11 +19,12 @@ export interface Fact {
18
19
  subjectIsOwner: boolean;
19
20
  occurredAt: string | null;
20
21
  supersedes: string | null;
21
- /** `name` is a bot's handle; `null` for a person. */
22
+ /** `name` is a bot's handle and `kind` whether it is an agent or a rule; both `null` for a person. */
22
23
  author: {
23
24
  type: Actor["type"];
24
25
  id: string;
25
26
  name: string | null;
27
+ kind: "agent" | "rule" | null;
26
28
  };
27
29
  model: string | null;
28
30
  confirmedBy: {
@@ -59,19 +61,22 @@ export interface FactsStore {
59
61
  project?: string;
60
62
  subject?: string;
61
63
  status?: Fact["status"];
64
+ proposedBy?: ProposedByFilter;
65
+ /** `created` lists the newest written first; by default the newest by when it happened. */
66
+ order?: "occurred" | "created";
62
67
  limit?: number;
63
68
  }): Promise<Fact[]>;
64
- /** The owner accepts a proposed fact; the fact it supersedes stops holding. */
65
- accept(reference: string): Promise<Fact>;
69
+ /** The owner, or an agent for the owner, accepts a proposed fact; the fact it supersedes stops holding. */
70
+ accept(reference: string, by: Decider, reason?: string): Promise<Fact>;
66
71
  /** An accepted decision no longer holds and nothing replaced it. */
67
- reverse(reference: string): Promise<Fact>;
68
- /** The owner turns down a proposed fact; it stays, marked `rejected`. */
69
- reject(reference: string): Promise<Fact>;
72
+ reverse(reference: string, by: Decider): Promise<Fact>;
73
+ /** The owner, or an agent for the owner, turns down a proposed fact; it stays, marked `rejected`. */
74
+ reject(reference: string, by: Decider, reason?: string): Promise<Fact>;
70
75
  /**
71
76
  * Erases the fact: its row, its links, its tags, and the memory it was migrated from, which still held its text.
72
77
  * A decision that replaced it no longer names it. A backup made before keeps it.
73
78
  */
74
- remove(reference: string): Promise<{
79
+ remove(reference: string, by: Decider): Promise<{
75
80
  ref: string;
76
81
  removed: true;
77
82
  links: number;
@@ -81,6 +86,9 @@ export interface FactsStore {
81
86
  }
82
87
  type FactOperations = {
83
88
  [K in keyof FactsStore]: FactsStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
89
+ } & {
90
+ /** Accepts as part of a decision already checked, such as a reviewed findings batch. */
91
+ acceptChecked(reference: string, by: Decider): Fact;
84
92
  };
85
93
  export declare const factOperationsOver: (context: StoreContext) => FactOperations;
86
94
  export declare const factsStoreOver: (context: StoreContext) => FactsStore;