@wirecat/cli-core 0.546.0 → 0.548.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +173 -0
- package/dist/calendar/cli/answers.d.ts +7 -2
- package/dist/calendar/cli/answers.js +12 -4
- package/dist/calendar/cli/index.js +9 -8
- package/dist/calendar/feeds.d.ts +19 -0
- package/dist/calendar/feeds.js +52 -9
- package/dist/calendar/source.d.ts +10 -2
- package/dist/calendar/sync.d.ts +2 -0
- package/dist/calendar/sync.js +10 -6
- package/dist/calendar/testing/memory.js +39 -0
- package/dist/cli/messenger/history-query.d.ts +10 -4
- package/dist/cli/messenger/history-query.js +87 -31
- package/dist/cli/messenger/messages-command.js +10 -4
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/errors/reasons.js +13 -1
- package/dist/mail/cli/index.js +4 -4
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/partial.d.ts +34 -0
- package/dist/mail/partial.js +94 -0
- package/dist/mail/server-search.js +17 -9
- package/dist/mail/source.d.ts +13 -2
- package/dist/mail/source.js +7 -1
- package/dist/mail/sync-types.d.ts +112 -0
- package/dist/mail/sync-types.js +1 -0
- package/dist/mail/sync.d.ts +4 -106
- package/dist/mail/sync.js +49 -4
- package/dist/mail/testing/fake.d.ts +3 -1
- package/dist/mail/testing/fake.js +10 -2
- package/dist/meetings/event-store.d.ts +10 -0
- package/dist/meetings/testing/event-store-contract.js +36 -0
- package/dist/services/deps.d.ts +0 -2
- package/dist/services/deps.js +0 -2
- package/dist/services/documents/ingestion.js +1 -1
- package/dist/services/documents/knowledge.d.ts +10 -6
- package/dist/services/documents/knowledge.js +25 -11
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +25 -23
- package/dist/services/proposal-tasks.d.ts +3 -2
- package/dist/services/proposal-tasks.js +5 -2
- package/dist/services/proposals/ids.d.ts +5 -0
- package/dist/services/proposals/ids.js +43 -0
- package/dist/services/proposals/index.d.ts +3 -0
- package/dist/services/proposals/index.js +2 -0
- package/dist/services/proposals/kinds/action.d.ts +7 -0
- package/dist/services/proposals/kinds/action.js +45 -0
- package/dist/services/proposals/kinds/fact.d.ts +4 -0
- package/dist/services/proposals/kinds/fact.js +43 -0
- package/dist/services/proposals/kinds/findings.d.ts +7 -0
- package/dist/services/proposals/kinds/findings.js +75 -0
- package/dist/services/proposals/kinds/key-document.d.ts +9 -0
- package/dist/services/proposals/kinds/key-document.js +75 -0
- package/dist/services/proposals/kinds/memory.d.ts +4 -0
- package/dist/services/proposals/kinds/memory.js +42 -0
- package/dist/services/proposals/kinds/relationship.d.ts +10 -0
- package/dist/services/proposals/kinds/relationship.js +82 -0
- package/dist/services/proposals/kinds/shared.d.ts +25 -0
- package/dist/services/proposals/kinds/shared.js +49 -0
- package/dist/services/proposals/registry.d.ts +9 -0
- package/dist/services/proposals/registry.js +129 -0
- package/dist/services/proposals/types.d.ts +85 -0
- package/dist/services/proposals/types.js +1 -0
- package/dist/services/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- package/dist/services/tasks.js +8 -4
- package/dist/store/decisions.d.ts +65 -0
- package/dist/store/decisions.js +55 -0
- package/dist/store/evidence-findings.d.ts +6 -6
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +2 -1
- package/dist/store/meeting-findings.d.ts +2 -0
- package/dist/store/messages.d.ts +4 -9
- package/dist/store/sqlite/accounts.d.ts +8 -0
- package/dist/store/sqlite/accounts.js +2 -2
- package/dist/store/sqlite/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- package/dist/store/sqlite/calendar-forget.d.ts +17 -0
- package/dist/store/sqlite/calendar-forget.js +68 -0
- package/dist/store/sqlite/calendar.js +8 -0
- package/dist/store/sqlite/email-locations.d.ts +16 -0
- package/dist/store/sqlite/email-locations.js +26 -0
- package/dist/store/sqlite/emails.d.ts +6 -2
- package/dist/store/sqlite/emails.js +2 -1
- package/dist/store/sqlite/evidence-findings.js +13 -3
- package/dist/store/sqlite/facts.d.ts +16 -8
- package/dist/store/sqlite/facts.js +48 -15
- package/dist/store/sqlite/findings-core.d.ts +2 -1
- package/dist/store/sqlite/findings-core.js +4 -4
- package/dist/store/sqlite/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +55 -15
- package/dist/store/sqlite/meeting-findings.js +6 -6
- package/dist/store/sqlite/memories.d.ts +12 -8
- package/dist/store/sqlite/memories.js +33 -10
- package/dist/store/sqlite/notes.d.ts +5 -1
- package/dist/store/sqlite/notes.js +18 -2
- package/dist/store/sqlite/open-store.d.ts +4 -1
- package/dist/store/sqlite/open-store.js +8 -2
- package/dist/store/sqlite/open.d.ts +3 -0
- package/dist/store/sqlite/proposed-actions.d.ts +21 -8
- package/dist/store/sqlite/proposed-actions.js +29 -9
- package/dist/store/sqlite/reads.d.ts +13 -2
- package/dist/store/sqlite/reads.js +8 -4
- package/dist/store/sqlite/relation-proposals.d.ts +34 -0
- package/dist/store/sqlite/relation-proposals.js +114 -0
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { BudgetStop, RunBudget } from "../background/budget.js";
|
|
2
|
+
import type { LoadEngine } from "../readers/types.js";
|
|
3
|
+
import type { AccountKey, MessageStore } from "../store/index.js";
|
|
4
|
+
import type { PartialOutcome } from "./partial.js";
|
|
5
|
+
import type { TagOutcome } from "./rule-tags.js";
|
|
6
|
+
import type { MailRule } from "./rules.js";
|
|
7
|
+
import type { DomainAge } from "./score.js";
|
|
8
|
+
export interface MailAccount {
|
|
9
|
+
/** The mailbox address: the store's account id. */
|
|
10
|
+
address: string;
|
|
11
|
+
mode?: "gmail" | "imap" | "mbox";
|
|
12
|
+
folders?: string[];
|
|
13
|
+
rules?: MailRule[];
|
|
14
|
+
}
|
|
15
|
+
/** What an index step reports, as memo's embedding does; the sync only passes it on. */
|
|
16
|
+
export interface MailIndexed {
|
|
17
|
+
chats: number;
|
|
18
|
+
chunks: number;
|
|
19
|
+
left: boolean;
|
|
20
|
+
notEmbedded?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface MailSyncResult {
|
|
23
|
+
account: string;
|
|
24
|
+
since: string;
|
|
25
|
+
listed: number;
|
|
26
|
+
saved: number;
|
|
27
|
+
alreadyStored: number;
|
|
28
|
+
/** New emails a skip rule kept out: never read, never stored. */
|
|
29
|
+
skipped: number;
|
|
30
|
+
/** New emails an envelope rule recorded: sender, date, size and labels, never their text. */
|
|
31
|
+
envelopes: number;
|
|
32
|
+
deleted: number;
|
|
33
|
+
/** False when `max` stopped the run before every new message was read; the next run continues. */
|
|
34
|
+
complete: boolean;
|
|
35
|
+
/** `partial`: a budgeted run stopped before the whole window was read; the next run goes on. */
|
|
36
|
+
state?: "partial" | "complete";
|
|
37
|
+
/** Why a budgeted run stopped early: its request budget, its deadline, or a cancel. */
|
|
38
|
+
stopped?: BudgetStop;
|
|
39
|
+
/** Messages of the window this run did not get to read: left by a budget, or by `max`. */
|
|
40
|
+
remaining?: number;
|
|
41
|
+
/** Requests a budgeted run spent. */
|
|
42
|
+
requests?: number;
|
|
43
|
+
/** Emails that could not be read or parsed; they are tried again next run. */
|
|
44
|
+
unreadable?: {
|
|
45
|
+
message: string;
|
|
46
|
+
reason: string;
|
|
47
|
+
}[];
|
|
48
|
+
/** Why stored messages missing at the source were left alone, when they were. */
|
|
49
|
+
deletionsSkipped?: string;
|
|
50
|
+
/** Folders whose server refused SEARCH: left unread, so the run is partial and proves no deletion. */
|
|
51
|
+
refusedFolders?: string[];
|
|
52
|
+
/** "resumed": only mail not done before was listed; "full": every message in the window was. */
|
|
53
|
+
pass?: "full" | "resumed";
|
|
54
|
+
indexed?: MailIndexed;
|
|
55
|
+
coverage?: {
|
|
56
|
+
folders: string[];
|
|
57
|
+
since: string;
|
|
58
|
+
until: string;
|
|
59
|
+
state: "window" | "partial";
|
|
60
|
+
identity: string;
|
|
61
|
+
};
|
|
62
|
+
attachments?: {
|
|
63
|
+
message: string;
|
|
64
|
+
part: number;
|
|
65
|
+
status: string;
|
|
66
|
+
}[];
|
|
67
|
+
/** Rule tags put on mail as an existing label the store pointed to. */
|
|
68
|
+
tagsReused?: TagOutcome["reused"];
|
|
69
|
+
/** Rule tags the store refused with nothing to reuse; that mail is stored untagged. */
|
|
70
|
+
tagsSkipped?: TagOutcome["skipped"];
|
|
71
|
+
/** Emails a server search stored from their first bytes, read whole this run, and those still partial. */
|
|
72
|
+
partial?: PartialOutcome;
|
|
73
|
+
/** `--retry-attachments`: emails read again for attachments a missing reader left unread. */
|
|
74
|
+
retried?: {
|
|
75
|
+
emails: number;
|
|
76
|
+
extracted: number;
|
|
77
|
+
stillFailed: number;
|
|
78
|
+
missing: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface MailSyncOptions {
|
|
82
|
+
account: MailAccount;
|
|
83
|
+
since: Date;
|
|
84
|
+
max?: number;
|
|
85
|
+
maxBodyChars?: number;
|
|
86
|
+
maxAttachmentChars?: number;
|
|
87
|
+
now?: () => number;
|
|
88
|
+
/** Domain registration dates already known, for the sender score a rule asks for. */
|
|
89
|
+
ages?: ReadonlyMap<string, DomainAge>;
|
|
90
|
+
loadEngine?: LoadEngine;
|
|
91
|
+
retryAttachments?: boolean;
|
|
92
|
+
/** List every message in the window, not only those an earlier run left: proves deletions, refreshes labels. */
|
|
93
|
+
rescan?: boolean;
|
|
94
|
+
/** `false` for a spot read, like the copy of a sent message: the run leaves the saved coverage as the last full run set it. */
|
|
95
|
+
saveCoverage?: boolean;
|
|
96
|
+
/** A window smaller than this is listed whole every run: it costs seconds and keeps flags and deletions current. */
|
|
97
|
+
resumeFrom?: number;
|
|
98
|
+
/** Runs after the mail is saved, so chats and mail still waiting for vectors are indexed; its answer is `indexed`. */
|
|
99
|
+
index?: (store: MessageStore, key: AccountKey) => Promise<MailIndexed>;
|
|
100
|
+
/** Stops between batches of emails, after saving the resume point. */
|
|
101
|
+
signal?: AbortSignal;
|
|
102
|
+
/**
|
|
103
|
+
* Makes the run budgeted: it reads the window in rounds, one request of the budget per call to the source, saves
|
|
104
|
+
* after each, stops when the budget says so and answers `state: "partial"`. A message the source could not give
|
|
105
|
+
* is tried once more at the end of the run and, if it still fails, left for the next one. `max` defaults to no limit.
|
|
106
|
+
*/
|
|
107
|
+
budget?: RunBudget;
|
|
108
|
+
/** Pauses before a round is read again after the store was locked; tests pass one that does not wait. */
|
|
109
|
+
wait?: (ms: number) => Promise<void>;
|
|
110
|
+
/** `false` for a spot read: emails a server search stored from their first bytes are not read whole this run. */
|
|
111
|
+
completePartial?: boolean;
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/mail/sync.d.ts
CHANGED
|
@@ -1,109 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
import { type MailRule } from "./rules.js";
|
|
6
|
-
import type { DomainAge } from "./score.js";
|
|
7
|
-
import type { MailSource } from "./source.js";
|
|
8
|
-
export interface MailAccount {
|
|
9
|
-
/** The mailbox address: the store's account id. */
|
|
10
|
-
address: string;
|
|
11
|
-
mode?: "gmail" | "imap" | "mbox";
|
|
12
|
-
folders?: string[];
|
|
13
|
-
rules?: MailRule[];
|
|
14
|
-
}
|
|
15
|
-
/** What an index step reports, as memo's embedding does; the sync only passes it on. */
|
|
16
|
-
export interface MailIndexed {
|
|
17
|
-
chats: number;
|
|
18
|
-
chunks: number;
|
|
19
|
-
left: boolean;
|
|
20
|
-
notEmbedded?: unknown;
|
|
21
|
-
}
|
|
22
|
-
export interface MailSyncResult {
|
|
23
|
-
account: string;
|
|
24
|
-
since: string;
|
|
25
|
-
listed: number;
|
|
26
|
-
saved: number;
|
|
27
|
-
alreadyStored: number;
|
|
28
|
-
/** New emails a skip rule kept out: never read, never stored. */
|
|
29
|
-
skipped: number;
|
|
30
|
-
/** New emails an envelope rule recorded: sender, date, size and labels, never their text. */
|
|
31
|
-
envelopes: number;
|
|
32
|
-
deleted: number;
|
|
33
|
-
/** False when `max` stopped the run before every new message was read; the next run continues. */
|
|
34
|
-
complete: boolean;
|
|
35
|
-
/** `partial`: a budgeted run stopped before the whole window was read; the next run goes on. */
|
|
36
|
-
state?: "partial" | "complete";
|
|
37
|
-
/** Why a budgeted run stopped early: its request budget, its deadline, or a cancel. */
|
|
38
|
-
stopped?: BudgetStop;
|
|
39
|
-
/** Messages of the window this run did not get to read: left by a budget, or by `max`. */
|
|
40
|
-
remaining?: number;
|
|
41
|
-
/** Requests a budgeted run spent. */
|
|
42
|
-
requests?: number;
|
|
43
|
-
/** Emails that could not be read or parsed; they are tried again next run. */
|
|
44
|
-
unreadable?: {
|
|
45
|
-
message: string;
|
|
46
|
-
reason: string;
|
|
47
|
-
}[];
|
|
48
|
-
/** Why stored messages missing at the source were left alone, when they were. */
|
|
49
|
-
deletionsSkipped?: string;
|
|
50
|
-
/** "resumed": only mail not done before was listed; "full": every message in the window was. */
|
|
51
|
-
pass?: "full" | "resumed";
|
|
52
|
-
indexed?: MailIndexed;
|
|
53
|
-
coverage?: {
|
|
54
|
-
folders: string[];
|
|
55
|
-
since: string;
|
|
56
|
-
until: string;
|
|
57
|
-
state: "window" | "partial";
|
|
58
|
-
identity: string;
|
|
59
|
-
};
|
|
60
|
-
attachments?: {
|
|
61
|
-
message: string;
|
|
62
|
-
part: number;
|
|
63
|
-
status: string;
|
|
64
|
-
}[];
|
|
65
|
-
/** Rule tags put on mail as an existing label the store pointed to. */
|
|
66
|
-
tagsReused?: TagOutcome["reused"];
|
|
67
|
-
/** Rule tags the store refused with nothing to reuse; that mail is stored untagged. */
|
|
68
|
-
tagsSkipped?: TagOutcome["skipped"];
|
|
69
|
-
/** `--retry-attachments`: emails read again for attachments a missing reader left unread. */
|
|
70
|
-
retried?: {
|
|
71
|
-
emails: number;
|
|
72
|
-
extracted: number;
|
|
73
|
-
stillFailed: number;
|
|
74
|
-
missing: number;
|
|
75
|
-
};
|
|
76
|
-
}
|
|
1
|
+
import type { MessageStore } from "../store/index.js";
|
|
2
|
+
import { type MailSource } from "./source.js";
|
|
3
|
+
import type { MailSyncOptions, MailSyncResult } from "./sync-types.js";
|
|
4
|
+
export type { MailAccount, MailIndexed, MailSyncOptions, MailSyncResult } from "./sync-types.js";
|
|
77
5
|
export { MAX_ATTACHMENT_CHARS } from "./attachments.js";
|
|
78
6
|
export { RETRYABLE_ATTACHMENTS } from "./retry.js";
|
|
79
|
-
export interface MailSyncOptions {
|
|
80
|
-
account: MailAccount;
|
|
81
|
-
since: Date;
|
|
82
|
-
max?: number;
|
|
83
|
-
maxBodyChars?: number;
|
|
84
|
-
maxAttachmentChars?: number;
|
|
85
|
-
now?: () => number;
|
|
86
|
-
/** Domain registration dates already known, for the sender score a rule asks for. */
|
|
87
|
-
ages?: ReadonlyMap<string, DomainAge>;
|
|
88
|
-
loadEngine?: LoadEngine;
|
|
89
|
-
retryAttachments?: boolean;
|
|
90
|
-
/** List every message in the window, not only those an earlier run left: proves deletions, refreshes labels. */
|
|
91
|
-
rescan?: boolean;
|
|
92
|
-
/** `false` for a spot read, like the copy of a sent message: the run leaves the saved coverage as the last full run set it. */
|
|
93
|
-
saveCoverage?: boolean;
|
|
94
|
-
/** A window smaller than this is listed whole every run: it costs seconds and keeps flags and deletions current. */
|
|
95
|
-
resumeFrom?: number;
|
|
96
|
-
/** Runs after the mail is saved, so chats and mail still waiting for vectors are indexed; its answer is `indexed`. */
|
|
97
|
-
index?: (store: MessageStore, key: AccountKey) => Promise<MailIndexed>;
|
|
98
|
-
/** Stops between batches of emails, after saving the resume point. */
|
|
99
|
-
signal?: AbortSignal;
|
|
100
|
-
/**
|
|
101
|
-
* Makes the run budgeted: it reads the window in rounds, one request of the budget per call to the source, saves
|
|
102
|
-
* after each, stops when the budget says so and answers `state: "partial"`. A message the source could not give
|
|
103
|
-
* is tried once more at the end of the run and, if it still fails, left for the next one. `max` defaults to no limit.
|
|
104
|
-
*/
|
|
105
|
-
budget?: RunBudget;
|
|
106
|
-
/** Pauses before a round is read again after the store was locked; tests pass one that does not wait. */
|
|
107
|
-
wait?: (ms: number) => Promise<void>;
|
|
108
|
-
}
|
|
109
7
|
export declare const syncMail: (source: MailSource, store: MessageStore, options: MailSyncOptions) => Promise<MailSyncResult>;
|
package/dist/mail/sync.js
CHANGED
|
@@ -12,11 +12,13 @@ import { dropMissing, tooManyGone } from "./gone.js";
|
|
|
12
12
|
import { mailHeadersOf } from "./headers.js";
|
|
13
13
|
import { flagsToRefresh, proveByLocation } from "./locations.js";
|
|
14
14
|
import { MAX_BODY_CHARS, parseMail } from "./parse.js";
|
|
15
|
+
import { completePartial } from "./partial.js";
|
|
15
16
|
import { FETCH_SLICE, keyOf, readQuick } from "./quick.js";
|
|
16
17
|
import { FULL_PASS_MS, MBOX_RESUME, mergeRanges, parsePoint, rangesOf, resumeKey, rulesKey, within, } from "./resume.js";
|
|
17
18
|
import { retryUnread } from "./retry.js";
|
|
18
19
|
import { ruleTagger, settleRuleTags } from "./rule-tags.js";
|
|
19
20
|
import { decide, ENVELOPE, envelopeBox, envelopeBoxOf, tagsOf } from "./rules.js";
|
|
21
|
+
import { searchRefused } from "./source.js";
|
|
20
22
|
const DAY = 86_400_000;
|
|
21
23
|
/** IMAP's SINCE counts whole days in the server's zone, so the window's first days are not trusted. */
|
|
22
24
|
const EDGE = 2 * DAY;
|
|
@@ -32,6 +34,29 @@ const LIST_SLICE = 200;
|
|
|
32
34
|
const STORE_BUSY_TRIES = 5;
|
|
33
35
|
const STORE_BUSY_WAIT_MS = 2_000;
|
|
34
36
|
export const syncMail = async (source, store, options) => {
|
|
37
|
+
const result = await syncRounds(source, store, options);
|
|
38
|
+
if (options.completePartial === false || options.retryAttachments || options.account.mode === "mbox")
|
|
39
|
+
return result;
|
|
40
|
+
if (result.stopped !== undefined || options.signal?.aborted)
|
|
41
|
+
return result;
|
|
42
|
+
const partial = await completePartial({
|
|
43
|
+
store,
|
|
44
|
+
source,
|
|
45
|
+
accountId: await store.accounts.saveAccount(mailAccountKey(options.account.address), {
|
|
46
|
+
name: options.account.address,
|
|
47
|
+
}),
|
|
48
|
+
address: options.account.address,
|
|
49
|
+
folders: () => mailFolders(options.account, source),
|
|
50
|
+
maxBodyChars: options.maxBodyChars ?? MAX_BODY_CHARS,
|
|
51
|
+
maxAttachmentChars: options.maxAttachmentChars ?? MAX_ATTACHMENT_CHARS,
|
|
52
|
+
loadEngine: options.loadEngine ?? importEngine,
|
|
53
|
+
now: (options.now ?? Date.now)(),
|
|
54
|
+
...(options.budget ? { take: () => options.budget?.take() ?? null } : {}),
|
|
55
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
56
|
+
});
|
|
57
|
+
return partial.completed || partial.left ? { ...result, partial } : result;
|
|
58
|
+
};
|
|
59
|
+
const syncRounds = async (source, store, options) => {
|
|
35
60
|
const { budget, index } = options;
|
|
36
61
|
if (budget === undefined || options.retryAttachments || options.account.mode === "mbox")
|
|
37
62
|
return syncRound(source, store, options);
|
|
@@ -45,6 +70,7 @@ export const syncMail = async (source, store, options) => {
|
|
|
45
70
|
unlisted: new Set(),
|
|
46
71
|
quick: new Map(),
|
|
47
72
|
quickFolders: new Set(),
|
|
73
|
+
refused: new Set(),
|
|
48
74
|
stopped: null,
|
|
49
75
|
remaining: 0,
|
|
50
76
|
asked: 0,
|
|
@@ -195,10 +221,16 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
195
221
|
/** A quick folder's stored mail as it was before this run changed anything, with the UID of each. */
|
|
196
222
|
const folderMail = new Map();
|
|
197
223
|
const refreshed = new Map();
|
|
224
|
+
/** Folders whose server refused SEARCH: nothing of them is known, so nothing of them is proved or moved. */
|
|
225
|
+
const refused = new Set();
|
|
198
226
|
const holdsMail = async (folder) => (await store.mail.emails({ accountId, mailbox: folder, limit: 1 })).length > 0;
|
|
199
227
|
for (const folder of folders) {
|
|
200
228
|
if (mboxPoint !== undefined)
|
|
201
229
|
break;
|
|
230
|
+
if (plan?.refused.has(folder)) {
|
|
231
|
+
refused.add(folder);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
202
234
|
let searched;
|
|
203
235
|
if (source.search !== undefined && !snapshot) {
|
|
204
236
|
searched = plan?.searches.get(folder);
|
|
@@ -209,7 +241,16 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
209
241
|
capped = true;
|
|
210
242
|
continue;
|
|
211
243
|
}
|
|
212
|
-
|
|
244
|
+
try {
|
|
245
|
+
searched = await source.search(since, folder);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (!searchRefused(error))
|
|
249
|
+
throw error;
|
|
250
|
+
refused.add(folder);
|
|
251
|
+
plan?.refused.add(folder);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
213
254
|
plan?.searches.set(folder, searched);
|
|
214
255
|
}
|
|
215
256
|
}
|
|
@@ -380,6 +421,8 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
380
421
|
let skipped;
|
|
381
422
|
if (snapshot)
|
|
382
423
|
skipped = "an mbox file is a snapshot; mail missing from it is not proven deleted";
|
|
424
|
+
else if (refused.size > 0)
|
|
425
|
+
skipped = `the server refused to search ${[...refused].join(", ")}; no deletion is checked until it answers`;
|
|
383
426
|
else if (plan !== undefined && (capped || plan.tried.size > plan.asked || plan.only !== undefined))
|
|
384
427
|
skipped = "this run read the window in parts; a run that lists all of it at once checks deletions";
|
|
385
428
|
else if (quickRan.size > 0) {
|
|
@@ -427,7 +470,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
427
470
|
skipped ??= older.skipped;
|
|
428
471
|
}
|
|
429
472
|
if (!snapshot)
|
|
430
|
-
await store.mail.setMailboxes(accountId, folders.map(mailbox), new Map([...foundFolders].map(([id, found]) => [id, [...found]])), startedAt);
|
|
473
|
+
await store.mail.setMailboxes(accountId, folders.filter((folder) => !refused.has(folder)).map(mailbox), new Map([...foundFolders].map(([id, found]) => [id, [...found]])), startedAt);
|
|
431
474
|
// A message's label list is whole, so every label the account has is replaced for each message read.
|
|
432
475
|
if (labelsOf.size && !snapshot) {
|
|
433
476
|
const known = (await store.mail.mailboxes(accountId)).filter(({ kind }) => kind === "label");
|
|
@@ -766,7 +809,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
766
809
|
],
|
|
767
810
|
]),
|
|
768
811
|
]), startedAt);
|
|
769
|
-
const incomplete = capped || (plan !== undefined && (plan.stopped !== null || plan.failed.size > 0));
|
|
812
|
+
const incomplete = capped || refused.size > 0 || (plan !== undefined && (plan.stopped !== null || plan.failed.size > 0));
|
|
770
813
|
const complete = !full && !incomplete && unreadable.length === 0 && (plan?.unreadable.size ?? 0) === 0;
|
|
771
814
|
const bulk = new Map(listed.filter((item) => item.headers?.bulk).map((item) => [item.messageId, [bulkBox.externalId]]));
|
|
772
815
|
if (bulk.size)
|
|
@@ -782,7 +825,8 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
782
825
|
state: complete ? "window" : "partial",
|
|
783
826
|
identity: account.mode === "imap" ? "message-id-or-uidvalidity-folder-uid" : "gmail-message-id",
|
|
784
827
|
};
|
|
785
|
-
|
|
828
|
+
// A refused folder says nothing of its mail, so the coverage an earlier run proved stays as it was.
|
|
829
|
+
if (!snapshot && saveCoverage && refused.size === 0)
|
|
786
830
|
await store.sync.setSyncState(key, "mail_coverage", JSON.stringify(coverage));
|
|
787
831
|
const tagging = tagger.outcome();
|
|
788
832
|
await settleRuleTags(store, accountId, rules, tagging);
|
|
@@ -802,6 +846,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
|
|
|
802
846
|
...(attachments.length ? { attachments } : {}),
|
|
803
847
|
...(unreadable.length ? { unreadable } : {}),
|
|
804
848
|
...(skipped === undefined ? {} : { deletionsSkipped: skipped }),
|
|
849
|
+
...(refused.size ? { refusedFolders: [...refused] } : {}),
|
|
805
850
|
...(searches.size > 0 ? { pass: resumed.size > 0 ? "resumed" : "full" } : {}),
|
|
806
851
|
...(tagging.reused.length ? { tagsReused: tagging.reused } : {}),
|
|
807
852
|
...(tagging.skipped.length ? { tagsSkipped: tagging.skipped } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FileStamp } from "../resume.js";
|
|
2
2
|
import type { MailCriteria } from "../server-query.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type MailSource } from "../source.js";
|
|
4
4
|
export type FakePerson = [name: string | null, address: string];
|
|
5
5
|
/** One message of a fake Gmail-like mailbox: its ids, envelope and the parts `rawOf` writes as RFC 5322. */
|
|
6
6
|
export interface FakeMail {
|
|
@@ -39,6 +39,8 @@ export interface FakeMailbox {
|
|
|
39
39
|
validity?: number;
|
|
40
40
|
/** Each answer of `search` and `list` ends in this failure. */
|
|
41
41
|
searchFails?: string;
|
|
42
|
+
/** Folders whose SEARCH the server refuses: `search` and `query` throw `SEARCH_REFUSED`. */
|
|
43
|
+
refusesSearch?: string[];
|
|
42
44
|
/** A refused header fetch fails the whole listing. */
|
|
43
45
|
headersFail?: boolean;
|
|
44
46
|
moveFails?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { GMAIL_ALL_MAIL } from "../folders.js";
|
|
3
3
|
import { headersOf } from "../headers.js";
|
|
4
|
+
import { SEARCH_REFUSED } from "../source.js";
|
|
4
5
|
/** As IMAP SEARCH matches: a case-blind substring of the field, every part required. */
|
|
5
6
|
const matches = (m, criteria) => {
|
|
6
7
|
const has = (field, value) => field.toLowerCase().includes(value.toLowerCase());
|
|
@@ -73,9 +74,14 @@ export const fakeMailSource = (mailbox) => {
|
|
|
73
74
|
throw failure(`Gmail has no such mailbox: ${folder}`);
|
|
74
75
|
return mailbox.messages.filter((m) => (folder === trash() ? m.folder === "trash" : m.folder !== "trash") && !m.absentFrom?.includes(folder));
|
|
75
76
|
};
|
|
77
|
+
const refuse = (folder) => {
|
|
78
|
+
if (mailbox.refusesSearch?.includes(folder))
|
|
79
|
+
throw new CliError("provider_error", `the mail server refused to search ${folder}`, { reason: SEARCH_REFUSED });
|
|
80
|
+
};
|
|
76
81
|
const search = async (since, folder) => {
|
|
77
82
|
if (mailbox.searchFails)
|
|
78
83
|
throw failure(mailbox.searchFails);
|
|
84
|
+
refuse(folder);
|
|
79
85
|
const from = Date.UTC(since.getUTCFullYear(), since.getUTCMonth(), since.getUTCDate());
|
|
80
86
|
return {
|
|
81
87
|
validity: String(mailbox.validity ?? 12),
|
|
@@ -137,6 +143,7 @@ export const fakeMailSource = (mailbox) => {
|
|
|
137
143
|
count("search");
|
|
138
144
|
if (mailbox.searchFails)
|
|
139
145
|
throw failure(mailbox.searchFails);
|
|
146
|
+
refuse(folder);
|
|
140
147
|
mailbox.queried?.push({ criteria, folder });
|
|
141
148
|
return {
|
|
142
149
|
validity: String(mailbox.validity ?? 12),
|
|
@@ -180,14 +187,15 @@ export const fakeMailSource = (mailbox) => {
|
|
|
180
187
|
if (messageIds.includes(m.messageId))
|
|
181
188
|
m.labels = [...new Set([...(m.labels ?? []), ...labels])];
|
|
182
189
|
},
|
|
183
|
-
fetchRaw: async (items) => {
|
|
190
|
+
fetchRaw: async (items, options) => {
|
|
184
191
|
count("fetchRaw");
|
|
185
192
|
mailbox.read?.push(...items.map(({ uid }) => uid));
|
|
186
193
|
return items.map((item) => {
|
|
187
194
|
const m = mailbox.messages.find((each) => each.uid === item.uid);
|
|
188
195
|
if (m === undefined || mailbox.unreadable?.includes(item.uid) || failsNow(item.uid))
|
|
189
196
|
return failure(`email ${item.uid} was not read`);
|
|
190
|
-
|
|
197
|
+
const raw = rawOf(m);
|
|
198
|
+
return options?.bytes === undefined ? raw : raw.subarray(0, options.bytes);
|
|
191
199
|
});
|
|
192
200
|
},
|
|
193
201
|
};
|
|
@@ -253,6 +253,16 @@ export interface EventStore {
|
|
|
253
253
|
saveCalendars(accountId: number, calendars: CalendarInput[], now: number): Promise<StoredCalendar[]>;
|
|
254
254
|
calendars(accountId: number): Promise<StoredCalendar[]>;
|
|
255
255
|
setCalendarSynced(calendarId: number, synced: boolean, now: number): Promise<void>;
|
|
256
|
+
/**
|
|
257
|
+
* The calendar and everything stored from it go, for a calendar that is not the owner's data (a followed feed): its
|
|
258
|
+
* entries, the events and series only it held, what was built from them (links, tags, search pieces, people seen
|
|
259
|
+
* only there) and its cursor. An event another calendar also holds stays, with that copy's fields; a meeting linked
|
|
260
|
+
* to a gone event stays, unlinked. Notes the owner wrote stay.
|
|
261
|
+
*/
|
|
262
|
+
forgetCalendar(calendarId: number, now: number): Promise<{
|
|
263
|
+
entries: number;
|
|
264
|
+
events: number;
|
|
265
|
+
}>;
|
|
256
266
|
/** One sync page, matched by rules 1–5, in one transaction. */
|
|
257
267
|
saveEntries(calendarId: number, entries: EntryInput[], now: number): Promise<StoredEntry[]>;
|
|
258
268
|
/**
|
|
@@ -192,6 +192,42 @@ export const eventStoreContract = (make) => {
|
|
|
192
192
|
assert.equal(occurrence?.entries[0]?.deletedAt, null);
|
|
193
193
|
assert.equal(await store.removeEntries(calendar, ["p"], CONTRACT_NOW + 1), 0);
|
|
194
194
|
}),
|
|
195
|
+
test("a forgotten calendar goes with its entries and the events only it held; a shared event keeps the other copy", async (fixture) => {
|
|
196
|
+
const { store, calendar, accountId } = fixture;
|
|
197
|
+
const feed = await second(fixture, "ics:example");
|
|
198
|
+
await store.saveEntries(calendar, [single("w1", later, { icalUid: "shared@example.com", title: "Mine" })], 2);
|
|
199
|
+
const series = {
|
|
200
|
+
...single("weekly", later, { title: "Weekly" }),
|
|
201
|
+
kind: "series",
|
|
202
|
+
recurrence: ["RRULE:FREQ=WEEKLY"],
|
|
203
|
+
};
|
|
204
|
+
await store.saveEntries(feed, [
|
|
205
|
+
single("f1", later, { icalUid: "shared@example.com", title: "Theirs", description: "longer text" }),
|
|
206
|
+
single("f2", later + HOUR, { title: "Only in the feed", participants: [{ email: "guest@example.com" }] }),
|
|
207
|
+
series,
|
|
208
|
+
], 3);
|
|
209
|
+
await store.saveSlots(feed, [
|
|
210
|
+
{
|
|
211
|
+
series: "weekly",
|
|
212
|
+
slots: [
|
|
213
|
+
{
|
|
214
|
+
...single("weekly@1", later + 2 * HOUR),
|
|
215
|
+
kind: "occurrence",
|
|
216
|
+
seriesExternalId: "weekly",
|
|
217
|
+
originalStartsAt: later + 2 * HOUR,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
], { from: earlier, to: later + 7 * 24 * HOUR }, "cursor", 3);
|
|
222
|
+
await store.setCursor(feed, "cursor", 3);
|
|
223
|
+
const forgotten = await store.forgetCalendar(feed, 4);
|
|
224
|
+
assert.deepEqual(forgotten, { entries: 4, events: 2 });
|
|
225
|
+
assert.deepEqual((await store.calendars(accountId)).map(({ externalId }) => externalId), ["primary"]);
|
|
226
|
+
const left = await store.events({ accountId });
|
|
227
|
+
assert.deepEqual(left.map(({ title, status }) => [title, status]), [["Mine", "confirmed"]]);
|
|
228
|
+
assert.equal((await store.event(left[0]?.id))?.entries.length, 1);
|
|
229
|
+
assert.equal(await store.entry(feed, "weekly"), null);
|
|
230
|
+
}),
|
|
195
231
|
test("a calendar's cursor is its own, and null forgets it", async (fixture) => {
|
|
196
232
|
const { store, calendar } = fixture;
|
|
197
233
|
const other = await second(fixture);
|
package/dist/services/deps.d.ts
CHANGED
|
@@ -70,8 +70,6 @@ export declare const storeIfOpen: (deps: ServiceDeps) => Promise<{
|
|
|
70
70
|
export declare const fromStore: (deps: Pick<ServiceDeps, "offline" | "reads">) => boolean;
|
|
71
71
|
/** What a store-mode read answers for a chat the store holds nothing of. */
|
|
72
72
|
export declare const nothingStored: (messenger: MessengerTraits) => string;
|
|
73
|
-
/** Where a store-mode messenger's history comes from, for what it cannot do. */
|
|
74
|
-
export declare const PUSHED = "this messenger's history is read from the local store";
|
|
75
73
|
export declare const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context|download`, `contacts list|show` and `review`";
|
|
76
74
|
/** Over a connection that is already open — an MCP session holds one for minutes. */
|
|
77
75
|
export declare const onlineDeps: (messenger: ServiceMessenger, adapter: MessengerAdapter, guard: SendGuard, where?: {
|
package/dist/services/deps.js
CHANGED
|
@@ -53,8 +53,6 @@ export const storeIfOpen = async (deps) => {
|
|
|
53
53
|
export const fromStore = (deps) => deps.offline || deps.reads === "store";
|
|
54
54
|
/** What a store-mode read answers for a chat the store holds nothing of. */
|
|
55
55
|
export const nothingStored = (messenger) => `nothing stored for this chat yet — keep \`${typedCommand(messenger.app.command, "serve")}\` running`;
|
|
56
|
-
/** Where a store-mode messenger's history comes from, for what it cannot do. */
|
|
57
|
-
export const PUSHED = "this messenger's history is read from the local store";
|
|
58
56
|
export const OFFLINE = "--offline answers only from what is kept locally: `chats list|show`, `messages list|show|context|download`, `contacts list|show` and `review`";
|
|
59
57
|
/** Over a connection that is already open — an MCP session holds one for minutes. */
|
|
60
58
|
export const onlineDeps = (messenger, adapter, guard, where = {}) => ({
|
|
@@ -37,7 +37,7 @@ export const documentIngestion = (store, options) => {
|
|
|
37
37
|
for (let batch = 0;; batch++) {
|
|
38
38
|
const memories = await store.memories.list({ subject: ref, status, limit: 500 });
|
|
39
39
|
for (const memory of memories)
|
|
40
|
-
await store.memories.markStale(memory.ref);
|
|
40
|
+
await store.memories.markStale(memory.ref, "owner");
|
|
41
41
|
if (memories.length < 500)
|
|
42
42
|
break;
|
|
43
43
|
if (batch >= 20)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
-
import type { Author, Fact, Link, LinkInput, LinkListFilter, Memory, MemoryInput, MessageStore, Note } from "../../store/index.js";
|
|
2
|
+
import type { Author, Decider, Fact, Link, LinkInput, LinkListFilter, Memory, MemoryInput, MessageStore, Note } from "../../store/index.js";
|
|
3
3
|
export interface DocumentKnowledge {
|
|
4
4
|
documents(principal: string, query: QueryAst | undefined, page: {
|
|
5
5
|
limit: number;
|
|
@@ -14,7 +14,7 @@ export interface DocumentKnowledge {
|
|
|
14
14
|
}): Promise<string[]>;
|
|
15
15
|
links(principal: string, ref: string, filter?: Omit<LinkListFilter, "from" | "to">): Promise<Link[]>;
|
|
16
16
|
addLink(principal: string, ref: string, input: Omit<LinkInput, "from" | "origin">): Promise<Link>;
|
|
17
|
-
removeLink(principal: string, ref: string, id: string): Promise<unknown>;
|
|
17
|
+
removeLink(principal: string, ref: string, id: string, by: Decider): Promise<unknown>;
|
|
18
18
|
notes(principal: string, ref: string, options?: {
|
|
19
19
|
search?: string;
|
|
20
20
|
limit?: number;
|
|
@@ -28,7 +28,7 @@ export interface DocumentKnowledge {
|
|
|
28
28
|
editNote(principal: string, ref: string, noteRef: string, text: string, revision: number): Promise<Note>;
|
|
29
29
|
removeNote(principal: string, ref: string, noteRef: string): Promise<unknown>;
|
|
30
30
|
addMemory(principal: string, ref: string, input: Pick<MemoryInput, "body" | "kind" | "scope" | "status" | "supersedes"> & {
|
|
31
|
-
author
|
|
31
|
+
author: MemoryInput["author"];
|
|
32
32
|
}): Promise<Memory>;
|
|
33
33
|
memories(principal: string, ref: string, options?: {
|
|
34
34
|
scope?: Memory["scope"];
|
|
@@ -36,7 +36,9 @@ export interface DocumentKnowledge {
|
|
|
36
36
|
status?: Memory["status"];
|
|
37
37
|
limit?: number;
|
|
38
38
|
}): Promise<Memory[]>;
|
|
39
|
-
memory(principal: string, ref: string, memoryRef: string, action?: "show"
|
|
39
|
+
memory(principal: string, ref: string, memoryRef: string, action?: "show"): Promise<Memory>;
|
|
40
|
+
/** `by` decides: the owner, or an agent for the owner that did not propose it. */
|
|
41
|
+
memory(principal: string, ref: string, memoryRef: string, action: "confirm" | "stale", by: Decider): Promise<Memory>;
|
|
40
42
|
searchMemories(principal: string, ref: string, query: string, scope?: Memory["scope"]): Promise<{
|
|
41
43
|
items: Memory[];
|
|
42
44
|
limit_reached: boolean;
|
|
@@ -45,7 +47,7 @@ export interface DocumentKnowledge {
|
|
|
45
47
|
statement: string;
|
|
46
48
|
type: Fact["type"];
|
|
47
49
|
scope?: Memory["scope"];
|
|
48
|
-
by
|
|
50
|
+
by: Author;
|
|
49
51
|
supersedes?: string;
|
|
50
52
|
occurredAt?: string;
|
|
51
53
|
}): Promise<Fact>;
|
|
@@ -54,6 +56,8 @@ export interface DocumentKnowledge {
|
|
|
54
56
|
status?: Fact["status"];
|
|
55
57
|
limit?: number;
|
|
56
58
|
}): Promise<Fact[]>;
|
|
57
|
-
fact(principal: string, ref: string, factRef: string, action?: "show"
|
|
59
|
+
fact(principal: string, ref: string, factRef: string, action?: "show"): Promise<Fact>;
|
|
60
|
+
/** `by` decides: the owner, or an agent for the owner that did not propose it; only the owner reverses. */
|
|
61
|
+
fact(principal: string, ref: string, factRef: string, action: "confirm" | "reverse", by: Decider): Promise<Fact>;
|
|
58
62
|
}
|
|
59
63
|
export declare const documentKnowledge: (store: Pick<MessageStore, "notes" | "facts" | "memories" | "knowledge">, folder: (principal: string) => Promise<string | undefined>) => DocumentKnowledge;
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
const writer = (by) => {
|
|
3
|
+
if (by === undefined)
|
|
4
|
+
throw new CliError("validation_error", "name who writes it: the owner or an agent; there is no default", {
|
|
5
|
+
reason: "proposal.decider-required",
|
|
6
|
+
});
|
|
7
|
+
return by;
|
|
8
|
+
};
|
|
9
|
+
const decider = (by) => {
|
|
10
|
+
if (by === undefined)
|
|
11
|
+
throw new CliError("validation_error", "a confirm names who decides: the owner or an agent with a reason", {
|
|
12
|
+
reason: "proposal.decider-required",
|
|
13
|
+
});
|
|
14
|
+
return by;
|
|
15
|
+
};
|
|
2
16
|
export const documentKnowledge = (store, folder) => {
|
|
3
17
|
const document = async (principal, ref) => {
|
|
4
18
|
const note = await store.notes.resolveNote(ref);
|
|
@@ -62,13 +76,13 @@ export const documentKnowledge = (store, folder) => {
|
|
|
62
76
|
},
|
|
63
77
|
addLink: async (principal, ref, input) => {
|
|
64
78
|
await document(principal, ref);
|
|
65
|
-
return store.notes.addLink({ ...input, from: ref, origin: "owner" });
|
|
79
|
+
return store.notes.addLink({ ...input, from: ref, origin: input.by === "owner" ? "owner" : "suggested" });
|
|
66
80
|
},
|
|
67
|
-
removeLink: async (principal, ref, id) => {
|
|
81
|
+
removeLink: async (principal, ref, id, by) => {
|
|
68
82
|
const links = await port.links(principal, ref);
|
|
69
83
|
if (!links.some((link) => link.id === id && link.origin === "owner"))
|
|
70
84
|
throw new CliError("not_found", "owner link is not attached to this document");
|
|
71
|
-
return store.notes.removeLink(id);
|
|
85
|
+
return store.notes.removeLink(id, by);
|
|
72
86
|
},
|
|
73
87
|
notes: async (principal, ref, options) => {
|
|
74
88
|
await document(principal, ref);
|
|
@@ -91,18 +105,18 @@ export const documentKnowledge = (store, folder) => {
|
|
|
91
105
|
await document(principal, ref);
|
|
92
106
|
if (input.supersedes)
|
|
93
107
|
await memoryFor(principal, ref, input.supersedes);
|
|
94
|
-
return store.memories.add({ ...input, subject: ref, evidence: [ref], author: input.author
|
|
108
|
+
return store.memories.add({ ...input, subject: ref, evidence: [ref], author: writer(input.author) });
|
|
95
109
|
},
|
|
96
110
|
memories: async (principal, ref, options) => {
|
|
97
111
|
await document(principal, ref);
|
|
98
112
|
return store.memories.list({ ...options, subject: ref });
|
|
99
113
|
},
|
|
100
|
-
memory: async (principal, ref, memoryRef, action = "show") => {
|
|
114
|
+
memory: async (principal, ref, memoryRef, action = "show", by) => {
|
|
101
115
|
const memory = await memoryFor(principal, ref, memoryRef);
|
|
102
116
|
if (action === "confirm")
|
|
103
|
-
return store.memories.confirm(memory.ref);
|
|
117
|
+
return store.memories.confirm(memory.ref, decider(by));
|
|
104
118
|
if (action === "stale")
|
|
105
|
-
return store.memories.markStale(memory.ref);
|
|
119
|
+
return store.memories.markStale(memory.ref, decider(by));
|
|
106
120
|
return memory;
|
|
107
121
|
},
|
|
108
122
|
searchMemories: async (principal, ref, query, scope) => {
|
|
@@ -117,7 +131,7 @@ export const documentKnowledge = (store, folder) => {
|
|
|
117
131
|
return store.facts.add({
|
|
118
132
|
type,
|
|
119
133
|
statement,
|
|
120
|
-
by: by
|
|
134
|
+
by: writer(by),
|
|
121
135
|
...(supersedes === undefined ? {} : { supersedes }),
|
|
122
136
|
...(occurredAt === undefined ? {} : { occurredAt }),
|
|
123
137
|
subject: ref,
|
|
@@ -129,12 +143,12 @@ export const documentKnowledge = (store, folder) => {
|
|
|
129
143
|
await document(principal, ref);
|
|
130
144
|
return store.facts.list({ ...options, subject: ref });
|
|
131
145
|
},
|
|
132
|
-
fact: async (principal, ref, factRef, action = "show") => {
|
|
146
|
+
fact: async (principal, ref, factRef, action = "show", by) => {
|
|
133
147
|
const fact = await factFor(principal, ref, factRef);
|
|
134
148
|
if (action === "confirm")
|
|
135
|
-
return store.facts.accept(fact.ref);
|
|
149
|
+
return store.facts.accept(fact.ref, decider(by));
|
|
136
150
|
if (action === "reverse")
|
|
137
|
-
return store.facts.reverse(fact.ref);
|
|
151
|
+
return store.facts.reverse(fact.ref, decider(by));
|
|
138
152
|
return fact;
|
|
139
153
|
},
|
|
140
154
|
};
|