@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,75 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { proposerOfAction } from "../../../store/index.js";
3
+ import { actionStatus, kept, proposedLink, pushed, storedStatus } from "./shared.js";
4
+ const linkRow = (link) => ({
5
+ id: `key:${link.id}`,
6
+ kind: "key_document",
7
+ status: "proposed",
8
+ project: link.to,
9
+ subject: link.from,
10
+ object: link.to,
11
+ summary: null,
12
+ proposedBy: { type: "unknown" },
13
+ reason: link.evidence,
14
+ evidence: [link.from],
15
+ score: null,
16
+ signals: null,
17
+ parts: null,
18
+ createdAt: link.createdAt,
19
+ });
20
+ const storedRow = (action) => {
21
+ const payload = action.payload;
22
+ return {
23
+ id: action.id,
24
+ kind: "key_document",
25
+ status: actionStatus(action.status),
26
+ project: payload.project,
27
+ subject: payload.ref,
28
+ object: payload.project,
29
+ summary: null,
30
+ proposedBy: proposerOfAction(action),
31
+ reason: action.reason ?? payload.reason ?? null,
32
+ evidence: [payload.ref],
33
+ score: null,
34
+ signals: null,
35
+ parts: null,
36
+ createdAt: action.createdAt,
37
+ };
38
+ };
39
+ /**
40
+ * A note, document or message proposed as one of a project's key documents. Stored `key_document` rows are the new
41
+ * home; older suggestions are unconfirmed `key` links, shown as `key:<id>`.
42
+ */
43
+ export const keyDocumentKind = (store) => {
44
+ const legacy = async () => (await store.notes.links({ kind: "related-to", role: "key", confirmed: false })).filter((link) => link.to?.startsWith("project:") && proposedLink(link.provenance));
45
+ const legacyLink = async (id) => {
46
+ const link = (await legacy()).find((one) => `key:${one.id}` === id);
47
+ if (!link)
48
+ throw new CliError("not_found", `no suggested key document ${id}`);
49
+ return link;
50
+ };
51
+ return {
52
+ kind: "key_document",
53
+ prefix: "key",
54
+ stored: ["key_document"],
55
+ list: async (filter) => {
56
+ const status = storedStatus(filter);
57
+ const stored = await store.proposedActions.list({
58
+ kinds: ["key_document"],
59
+ ...(status === undefined ? {} : { status }),
60
+ ...pushed(filter),
61
+ });
62
+ const links = (filter.status ?? "proposed") === "rejected" ? [] : await legacy();
63
+ return kept([...stored.map(storedRow), ...links.map(linkRow)], filter);
64
+ },
65
+ get: async (id) => id.startsWith("key:") ? linkRow(await legacyLink(id)) : storedRow(await store.proposedActions.get(id)),
66
+ decide: async (id, { confirm, by, reason }) => {
67
+ const status = confirm ? "confirmed" : "rejected";
68
+ const decision = { confirm, by, ...(reason === undefined ? {} : { reason }) };
69
+ const link = id.startsWith("key:")
70
+ ? await store.knowledge.decideSuggestedLink(id.slice("key:".length), decision)
71
+ : (await store.knowledge.decideLinkProposal(id, decision)).link;
72
+ return { id, kind: "key_document", status, made: link ? [`link:${link.id}`] : [] };
73
+ },
74
+ };
75
+ };
@@ -0,0 +1,4 @@
1
+ import type { MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ /** Memories keep their own table; rejecting one marks it stale. */
4
+ export declare const memoryKind: (store: Pick<MessageStore, "memories">) => ProposalKind;
@@ -0,0 +1,42 @@
1
+ import { kept, proposerOfAuthor, pushed } from "./shared.js";
2
+ /** A stale memory is the one a person turned down; a superseded one held until another replaced it. */
3
+ const STATUS = {
4
+ proposed: "proposed",
5
+ confirmed: "confirmed",
6
+ superseded: "confirmed",
7
+ stale: "rejected",
8
+ };
9
+ const rowOf = (memory) => ({
10
+ id: memory.ref,
11
+ kind: "memory",
12
+ status: STATUS[memory.status],
13
+ project: memory.project,
14
+ subject: memory.subject ?? memory.ref,
15
+ object: null,
16
+ summary: null,
17
+ proposedBy: proposerOfAuthor(memory.author),
18
+ reason: null,
19
+ evidence: memory.evidence.slice(0, 5),
20
+ score: null,
21
+ signals: null,
22
+ parts: null,
23
+ createdAt: memory.createdAt,
24
+ });
25
+ /** Memories keep their own table; rejecting one marks it stale. */
26
+ export const memoryKind = (store) => ({
27
+ kind: "memory",
28
+ prefix: "memory",
29
+ list: async (filter) => {
30
+ const status = filter.status ?? "proposed";
31
+ const memories = await store.memories.list({
32
+ ...pushed(filter),
33
+ ...(status === "all" ? {} : { status: status === "rejected" ? "stale" : status }),
34
+ });
35
+ return kept(memories.map(rowOf), filter);
36
+ },
37
+ get: async (id) => rowOf(await store.memories.get(id)),
38
+ decide: async (id, { confirm, by }) => {
39
+ const memory = confirm ? await store.memories.confirm(id, by) : await store.memories.markStale(id, by);
40
+ return { id, kind: "memory", status: confirm ? "confirmed" : "rejected", made: confirm ? [memory.ref] : [] };
41
+ },
42
+ });
@@ -0,0 +1,10 @@
1
+ import type { MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ type RelationshipStore = Pick<MessageStore, "knowledge" | "notes" | "proposedActions">;
4
+ /**
5
+ * A person, organization, chat or mail thread proposed as related to another thing. New proposals are stored
6
+ * `relationship` rows; suggestions written before them are unconfirmed links, shown as `link:<id>` until the store
7
+ * moves them.
8
+ */
9
+ export declare const relationshipKind: (store: RelationshipStore) => ProposalKind;
10
+ export {};
@@ -0,0 +1,82 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { proposerOfAction } from "../../../store/index.js";
3
+ import { actionStatus, kept, projectOf, proposedLink, pushed, storedStatus } from "./shared.js";
4
+ const linkRow = (link) => ({
5
+ id: `link:${link.id}`,
6
+ kind: "relationship",
7
+ status: "proposed",
8
+ project: projectOf(link.to, link.from),
9
+ subject: link.from,
10
+ object: link.to,
11
+ summary: link.role,
12
+ proposedBy: { type: "unknown" },
13
+ reason: link.evidence,
14
+ evidence: [],
15
+ score: null,
16
+ signals: null,
17
+ parts: null,
18
+ createdAt: link.createdAt,
19
+ });
20
+ const storedRow = (action) => {
21
+ const payload = action.payload;
22
+ return {
23
+ id: action.id,
24
+ kind: "relationship",
25
+ status: actionStatus(action.status),
26
+ project: projectOf(payload.to, payload.from),
27
+ subject: payload.from,
28
+ object: payload.to,
29
+ summary: payload.role ?? null,
30
+ proposedBy: proposerOfAction(action),
31
+ reason: action.reason,
32
+ evidence: (payload.evidence ?? []).slice(0, 5),
33
+ score: payload.score ?? null,
34
+ signals: payload.signals ?? null,
35
+ parts: null,
36
+ createdAt: action.createdAt,
37
+ };
38
+ };
39
+ /**
40
+ * A person, organization, chat or mail thread proposed as related to another thing. New proposals are stored
41
+ * `relationship` rows; suggestions written before them are unconfirmed links, shown as `link:<id>` until the store
42
+ * moves them.
43
+ */
44
+ export const relationshipKind = (store) => {
45
+ const legacy = async () => {
46
+ const [members, related] = await Promise.all([
47
+ store.notes.links({ kind: "member-of", confirmed: false }),
48
+ store.notes.links({ kind: "related-to", confirmed: false }),
49
+ ]);
50
+ return [...members, ...related].filter((link) => link.to !== null && link.role !== "key" && proposedLink(link.provenance));
51
+ };
52
+ const legacyLink = async (id) => {
53
+ const link = (await legacy()).find((one) => `link:${one.id}` === id);
54
+ if (!link)
55
+ throw new CliError("not_found", `no proposed relationship ${id}`);
56
+ return link;
57
+ };
58
+ return {
59
+ kind: "relationship",
60
+ prefix: "link",
61
+ stored: ["relationship"],
62
+ list: async (filter) => {
63
+ const status = storedStatus(filter);
64
+ const stored = await store.proposedActions.list({
65
+ kinds: ["relationship"],
66
+ ...(status === undefined ? {} : { status }),
67
+ ...pushed(filter),
68
+ });
69
+ const links = (filter.status ?? "proposed") === "rejected" ? [] : await legacy();
70
+ return kept([...stored.map(storedRow), ...links.map(linkRow)], filter);
71
+ },
72
+ get: async (id) => id.startsWith("link:") ? linkRow(await legacyLink(id)) : storedRow(await store.proposedActions.get(id)),
73
+ decide: async (id, { confirm, by, reason }) => {
74
+ const status = confirm ? "confirmed" : "rejected";
75
+ const decision = { confirm, by, ...(reason === undefined ? {} : { reason }) };
76
+ const link = id.startsWith("link:")
77
+ ? await store.knowledge.decideSuggestedLink(id.slice("link:".length), decision)
78
+ : (await store.knowledge.decideLinkProposal(id, decision)).link;
79
+ return { id, kind: "relationship", status, made: link ? [`relationship:${link.id}`] : [] };
80
+ },
81
+ };
82
+ };
@@ -0,0 +1,25 @@
1
+ import type { Proposer } from "../../../store/decisions.js";
2
+ import type { ProposalFilter, ProposalRow } from "../types.js";
3
+ /** A stored author as a proposer, with the kind read from its bot row. */
4
+ export declare const proposerOfAuthor: (author: {
5
+ type: "person" | "bot";
6
+ id: string;
7
+ name: string | null;
8
+ kind: "agent" | "rule" | null;
9
+ }) => Proposer;
10
+ /** What a kind keeps of the rows it read: the filter's status and proposer, newest first, at most `limit`. */
11
+ export declare const kept: (rows: ProposalRow[], filter: ProposalFilter) => ProposalRow[];
12
+ /** The filters every store list takes, so a page is filled by the query and not cut after it. */
13
+ export declare const pushed: (filter: ProposalFilter) => {
14
+ project?: string | undefined;
15
+ proposedBy?: "agent" | "rule" | {
16
+ bot: string;
17
+ } | undefined;
18
+ limit: number;
19
+ };
20
+ /** A suggestion an agent or a rule wrote; Drive's editor links and other records stay out of the inbox. */
21
+ export declare const proposedLink: (provenance: string | null) => boolean;
22
+ export declare const projectOf: (...refs: (string | null | undefined)[]) => string | null;
23
+ export declare const actionStatus: (status: string) => ProposalRow["status"];
24
+ /** The stored statuses a filter's status reads, so the store does the filtering when it can. */
25
+ export declare const storedStatus: (filter: ProposalFilter) => "proposed" | "rejected" | undefined;
@@ -0,0 +1,49 @@
1
+ /** A stored author as a proposer, with the kind read from its bot row. */
2
+ export const proposerOfAuthor = (author) => {
3
+ if (author.type === "person")
4
+ return { type: "person", id: author.id };
5
+ if (author.name === null)
6
+ return { type: "unknown" };
7
+ return { type: "bot", id: author.id, name: author.name, kind: author.kind ?? "agent" };
8
+ };
9
+ const byMatches = (row, by) => {
10
+ if (by === undefined)
11
+ return true;
12
+ const proposer = row.proposedBy;
13
+ if (proposer.type !== "bot")
14
+ return false;
15
+ if (by === "agent" || by === "rule")
16
+ return proposer.kind === by;
17
+ return proposer.name === by.bot;
18
+ };
19
+ /** What a kind keeps of the rows it read: the filter's status and proposer, newest first, at most `limit`. */
20
+ export const kept = (rows, filter) => {
21
+ const status = filter.status ?? "proposed";
22
+ return rows
23
+ .filter((row) => (status === "all" || row.status === status) && byMatches(row, filter.by))
24
+ .filter((row) => filter.project === undefined || row.project === filter.project)
25
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
26
+ .slice(0, filter.limit);
27
+ };
28
+ /** The filters every store list takes, so a page is filled by the query and not cut after it. */
29
+ export const pushed = (filter) => ({
30
+ ...(filter.project === undefined ? {} : { project: filter.project }),
31
+ ...(filter.by === undefined ? {} : { proposedBy: filter.by }),
32
+ limit: Math.min(filter.limit, 500),
33
+ });
34
+ /** A suggestion an agent or a rule wrote; Drive's editor links and other records stay out of the inbox. */
35
+ export const proposedLink = (provenance) => /^(agent|rule):/.test(provenance ?? "");
36
+ export const projectOf = (...refs) => refs.find((ref) => typeof ref === "string" && ref.startsWith("project:")) ?? null;
37
+ const ACTION_STATUS = {
38
+ proposed: "proposed",
39
+ approved: "confirmed",
40
+ executed: "confirmed",
41
+ failed: "confirmed",
42
+ rejected: "rejected",
43
+ };
44
+ export const actionStatus = (status) => ACTION_STATUS[status] ?? "proposed";
45
+ /** The stored statuses a filter's status reads, so the store does the filtering when it can. */
46
+ export const storedStatus = (filter) => {
47
+ const status = filter.status ?? "proposed";
48
+ return status === "proposed" || status === "rejected" ? status : undefined;
49
+ };
@@ -0,0 +1,9 @@
1
+ import type { MessageStore } from "../../store/store.js";
2
+ import type { ProposalRegistry } from "./types.js";
3
+ /** An empty registry: the host registers its kinds. `proposalRegistry` is this with the core kinds. */
4
+ export declare const emptyProposalRegistry: (store: Pick<MessageStore, "proposedActions" | "decisionPolicy">) => ProposalRegistry & {
5
+ claimed(): string[];
6
+ };
7
+ /** Every proposal kind core knows, behind one check of who may decide. */
8
+ /** The policy is the store's, set when it was opened, so the registry and the store methods never disagree. */
9
+ export declare const proposalRegistry: (store: MessageStore) => ProposalRegistry;
@@ -0,0 +1,129 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { mayDecide } from "../../store/decisions.js";
3
+ import { actionKind } from "./kinds/action.js";
4
+ import { factKind } from "./kinds/fact.js";
5
+ import { findingsKind } from "./kinds/findings.js";
6
+ import { keyDocumentKind } from "./kinds/key-document.js";
7
+ import { memoryKind } from "./kinds/memory.js";
8
+ import { relationshipKind } from "./kinds/relationship.js";
9
+ const PREFIX = /^([a-z][a-z_]*):/;
10
+ const refusalOf = (id, error) => {
11
+ if (!(error instanceof CliError))
12
+ throw error;
13
+ const code = error.code === "not_found" || error.code === "permission_error" ? error.code : "validation_error";
14
+ const reason = error.details?.reason;
15
+ return { id, code, reason: typeof reason === "string" ? reason : code, message: error.message };
16
+ };
17
+ /** An empty registry: the host registers its kinds. `proposalRegistry` is this with the core kinds. */
18
+ export const emptyProposalRegistry = (store) => {
19
+ const policy = store.decisionPolicy;
20
+ const kinds = [];
21
+ const claimed = () => kinds.flatMap((kind) => kind.stored ?? []);
22
+ const kindOf = async (id) => {
23
+ const prefix = PREFIX.exec(id)?.[1];
24
+ const found = prefix !== undefined
25
+ ? kinds.find((kind) => kind.prefix === prefix)
26
+ : await (async () => {
27
+ const stored = (await store.proposedActions.get(id)).kind;
28
+ return (kinds.find((kind) => kind.stored?.includes(stored)) ??
29
+ kinds.find((kind) => kind.prefix === undefined && kind.stored === undefined));
30
+ })();
31
+ if (!found)
32
+ throw new CliError("not_found", `no proposal ${id}`, { reason: "proposal.unknown-kind" });
33
+ return found;
34
+ };
35
+ const registry = {
36
+ claimed,
37
+ register: (kind) => {
38
+ if (kinds.some((one) => one.kind === kind.kind))
39
+ throw new CliError("configuration_error", `proposal kind ${kind.kind} is registered twice`);
40
+ if (kind.prefix !== undefined && kinds.some((one) => one.prefix === kind.prefix))
41
+ throw new CliError("configuration_error", `proposal prefix ${kind.prefix} is registered twice`);
42
+ kinds.push(kind);
43
+ },
44
+ list: async (filter) => {
45
+ if (!Number.isSafeInteger(filter.limit) || filter.limit < 1 || filter.limit > 50)
46
+ throw new CliError("validation_error", "a proposal list takes 1–50 items");
47
+ if (!Number.isSafeInteger(filter.offset) || filter.offset < 0)
48
+ throw new CliError("validation_error", "a proposal list takes a nonnegative offset");
49
+ const asked = filter.kinds === undefined ? kinds : kinds.filter((kind) => filter.kinds?.includes(kind.kind));
50
+ const wide = { ...filter, offset: 0, limit: filter.offset + filter.limit + 1 };
51
+ const rows = (await Promise.all(asked.map((kind) => kind.list(wide)))).flat();
52
+ rows.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
53
+ const page = rows.slice(filter.offset, filter.offset + filter.limit + 1);
54
+ return { items: page.slice(0, filter.limit), hasMore: page.length > filter.limit };
55
+ },
56
+ get: async (id) => (await kindOf(id)).get(id),
57
+ decide: async (ids, decision) => {
58
+ const parts = new Map();
59
+ const whole = new Set();
60
+ const order = [];
61
+ for (const id of ids) {
62
+ const [batch, part] = id.split("/");
63
+ if (!batch)
64
+ continue;
65
+ if (!parts.has(batch)) {
66
+ parts.set(batch, []);
67
+ order.push(batch);
68
+ }
69
+ if (part === undefined)
70
+ whole.add(batch);
71
+ else
72
+ parts.get(batch)?.push(part);
73
+ }
74
+ const decided = [];
75
+ const refused = [];
76
+ const refuseIds = (id, message) => refused.push({ id, code: "validation_error", reason: "proposal.ids", message });
77
+ for (const id of order) {
78
+ const chosen = parts.get(id) ?? [];
79
+ if (chosen.length && whole.has(id)) {
80
+ refuseIds(id, `name ${id} or some of its parts (${id}/${chosen[0]}), not both`);
81
+ continue;
82
+ }
83
+ try {
84
+ const kind = await kindOf(id);
85
+ if (chosen.length && !kind.parts) {
86
+ refuseIds(id, `a ${kind.kind} proposal has no parts; name ${id} alone`);
87
+ continue;
88
+ }
89
+ const row = await kind.get(id);
90
+ if (row.status !== "proposed") {
91
+ refused.push({
92
+ id,
93
+ code: "validation_error",
94
+ reason: "proposal.wrong-state",
95
+ message: `${id} is already ${row.status}`,
96
+ });
97
+ continue;
98
+ }
99
+ const refusal = mayDecide(row, decision.by, policy);
100
+ if (refusal) {
101
+ refused.push(refusal);
102
+ continue;
103
+ }
104
+ decided.push(await kind.decide(id, chosen.length ? { ...decision, parts: chosen } : decision));
105
+ }
106
+ catch (error) {
107
+ refused.push(refusalOf(id, error));
108
+ }
109
+ }
110
+ return { decided, refused };
111
+ },
112
+ };
113
+ return registry;
114
+ };
115
+ /** Every proposal kind core knows, behind one check of who may decide. */
116
+ /** The policy is the store's, set when it was opened, so the registry and the store methods never disagree. */
117
+ export const proposalRegistry = (store) => {
118
+ const registry = emptyProposalRegistry(store);
119
+ for (const kind of [
120
+ relationshipKind(store),
121
+ keyDocumentKind(store),
122
+ factKind(store),
123
+ memoryKind(store),
124
+ findingsKind(store),
125
+ ])
126
+ registry.register(kind);
127
+ registry.register(actionKind(store, registry.claimed));
128
+ return registry;
129
+ };
@@ -0,0 +1,85 @@
1
+ import type { Decider, Proposer, Refusal } from "../../store/decisions.js";
2
+ export interface ProposalRow {
3
+ /** `17070` for a stored proposal; `fact:512`, `memory:88`, `link:17` and `key:9` for the kinds kept elsewhere. */
4
+ id: string;
5
+ kind: string;
6
+ status: "proposed" | "confirmed" | "rejected";
7
+ project: string | null;
8
+ /** The reference it is about. */
9
+ subject: string;
10
+ /** The reference it links to, when it links. */
11
+ object: string | null;
12
+ /** A fact's statement or a finding count; never message text. */
13
+ summary: string | null;
14
+ proposedBy: Proposer;
15
+ reason: string | null;
16
+ /** At most 5 references. */
17
+ evidence: string[];
18
+ score: number | null;
19
+ signals: {
20
+ scorer: string;
21
+ weight: number;
22
+ why: string;
23
+ }[] | null;
24
+ parts: {
25
+ id: string;
26
+ kind: string;
27
+ summary: string;
28
+ outcome: "accepted" | "rejected" | null;
29
+ }[] | null;
30
+ createdAt: string;
31
+ }
32
+ export interface ProposalFilter {
33
+ project?: string;
34
+ kinds?: string[];
35
+ by?: "agent" | "rule" | {
36
+ bot: string;
37
+ };
38
+ status?: ProposalRow["status"] | "all";
39
+ limit: number;
40
+ offset: number;
41
+ }
42
+ export interface Decision {
43
+ confirm: boolean;
44
+ by: Decider;
45
+ /** Why, kept with the decision; a memory has nowhere to keep it. */
46
+ reason?: string;
47
+ /** The findings of a batch to accept; the rest are rejected. */
48
+ parts?: string[];
49
+ }
50
+ export interface Decided {
51
+ id: string;
52
+ kind: string;
53
+ status: "confirmed" | "rejected";
54
+ /** References the decision made: `relationship:2203`, `fact:530`, `task:…`. */
55
+ made: string[];
56
+ }
57
+ /**
58
+ * One kind of proposal. `prefix` names its own id space (`fact` for `fact:512`); a kind stored in `proposed_actions`
59
+ * names the stored kinds it owns in `stored` and has bare number ids. A new kind registers itself; nothing else changes.
60
+ */
61
+ export interface ProposalKind {
62
+ readonly kind: string;
63
+ readonly prefix?: string;
64
+ readonly stored?: readonly string[];
65
+ /** Whether its proposals have parts decided on their own, as a findings batch has findings. */
66
+ readonly parts?: boolean;
67
+ /** Bounded: at most `offset + limit` rows, newest first. */
68
+ list(filter: ProposalFilter): Promise<ProposalRow[]>;
69
+ get(id: string): Promise<ProposalRow>;
70
+ /** Called only after `mayDecide` allowed it. */
71
+ decide(id: string, decision: Decision): Promise<Decided>;
72
+ }
73
+ export interface ProposalRegistry {
74
+ register(kind: ProposalKind): void;
75
+ list(filter: ProposalFilter): Promise<{
76
+ items: ProposalRow[];
77
+ hasMore: boolean;
78
+ }>;
79
+ get(id: string): Promise<ProposalRow>;
80
+ /** Each id on its own: one refusal never undoes the others. */
81
+ decide(ids: string[], decision: Decision): Promise<{
82
+ decided: Decided[];
83
+ refused: Refusal[];
84
+ }>;
85
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -2,12 +2,15 @@ import type { Id } from "../domain/models.js";
2
2
  import type { ServiceDeps } from "./deps.js";
3
3
  import type { SearchQuery } from "./messages.js";
4
4
  export declare const SERVER_SEARCH_KEY = "messages.server-search";
5
- /** `maxMessageBytes`: a candidate larger than this is not downloaded; a sync reads it, and the answer says so. */
5
+ /**
6
+ * `textBytes`: a mail candidate is read up to this many bytes, its headers and text; one cut short is stored partial
7
+ * and the next sync reads it whole.
8
+ */
6
9
  export declare const SERVER_BOUNDS: {
7
10
  timeMs: number;
8
11
  perCall: number;
9
12
  maxCalls: number;
10
- maxMessageBytes: number;
13
+ textBytes: number;
11
14
  };
12
15
  export declare const DEFAULT_BACKEND = "both";
13
16
  export type Backend = "archive" | "server" | "both";
@@ -26,8 +29,8 @@ export interface ServerSearched {
26
29
  chat: string | null;
27
30
  reason: "rate_limited" | "search_failed" | "time_or_abort_bound";
28
31
  }[];
29
- /** Candidates larger than `SERVER_BOUNDS.maxMessageBytes`, left on the server unread. */
30
- tooLarge?: number;
32
+ /** Candidates stored from their first `SERVER_BOUNDS.textBytes`, without the attachments; a sync reads them whole. */
33
+ partial?: number;
31
34
  complete: boolean;
32
35
  }
33
36
  export type HitSource = "archive" | "server" | "both";
@@ -2,8 +2,11 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { capability } from "../ports/messenger.js";
3
3
  import { prepareLucene } from "./messages-search.js";
4
4
  export const SERVER_SEARCH_KEY = "messages.server-search";
5
- /** `maxMessageBytes`: a candidate larger than this is not downloaded; a sync reads it, and the answer says so. */
6
- export const SERVER_BOUNDS = { timeMs: 5_000, perCall: 100, maxCalls: 3, maxMessageBytes: 5_000_000 };
5
+ /**
6
+ * `textBytes`: a mail candidate is read up to this many bytes, its headers and text; one cut short is stored partial
7
+ * and the next sync reads it whole.
8
+ */
9
+ export const SERVER_BOUNDS = { timeMs: 5_000, perCall: 100, maxCalls: 3, textBytes: 262_144 };
7
10
  export const DEFAULT_BACKEND = "both";
8
11
  export const sourceKey = (chatId, id) => JSON.stringify([chatId, id]);
9
12
  const merge = (one, other) => {
@@ -260,7 +260,9 @@ export const tasksService = (deps, categories) => {
260
260
  const link = {
261
261
  from: (await store.tasks.details(target.id)).ref,
262
262
  kind: "evidence",
263
- origin: origin === "owner" ? "owner" : "suggested",
263
+ ...(origin === "owner"
264
+ ? { origin: "owner", by: "owner" }
265
+ : { origin: "suggested", confirmed: false }),
264
266
  };
265
267
  try {
266
268
  await store.notes.addLink({ ...link, to: note ? note.ref : source });
@@ -448,17 +450,19 @@ export const tasksService = (deps, categories) => {
448
450
  account: account.account,
449
451
  chat: await storedChatId(deps.messenger, chat, store, account, { includeLeft: true }),
450
452
  })));
451
- const origin = by === "owner" ? "owner" : "suggested";
453
+ const origin = by === "owner"
454
+ ? { origin: "owner", by: "owner" }
455
+ : { origin: "suggested", confirmed: false };
452
456
  for (const target of [...written, ...chatRefs]) {
453
457
  try {
454
- await store.notes.addLink({ from, to: target, kind: kind, origin });
458
+ await store.notes.addLink({ from, to: target, kind: kind, ...origin });
455
459
  }
456
460
  catch (error) {
457
461
  // Only text that names nothing typed, or a message not saved yet; a mistyped `person:9` is refused.
458
462
  const unsaved = error instanceof CliError && error.code === "not_found" && target.startsWith("msg:");
459
463
  if (!unsaved && !(error instanceof CliError && error.code === "validation_error"))
460
464
  throw error;
461
- await store.notes.addLink({ from, targetText: target, kind: kind, origin });
465
+ await store.notes.addLink({ from, targetText: target, kind: kind, ...origin });
462
466
  }
463
467
  }
464
468
  if (!written.some((target) => target.startsWith("msg:")))