@wirecat/cli-core 0.559.0 → 0.560.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 +43 -0
- package/dist/background/schedules.d.ts +2 -0
- package/dist/cli/effects.d.ts +13 -2
- package/dist/cli/effects.js +23 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/meeting-findings-input.d.ts +3 -0
- package/dist/cli/meeting-findings-input.js +4 -0
- package/dist/cli/messenger/conversation-answers.d.ts +3 -0
- package/dist/cli/messenger/message-answers.d.ts +3 -0
- package/dist/cli/messenger/message-writes.d.ts +6 -0
- package/dist/cli/schedule/schedule-command.d.ts +6 -1
- package/dist/cli/schedule/schedule-command.js +24 -2
- package/dist/entities/contact-context.d.ts +6 -0
- package/dist/entities/conversation-results.d.ts +6 -0
- package/dist/entities/evidence.d.ts +24 -0
- package/dist/entities/index.d.ts +190 -0
- package/dist/entities/index.js +3 -0
- package/dist/entities/listener.d.ts +12 -0
- package/dist/entities/mail-findings.d.ts +116 -0
- package/dist/entities/mail-findings.js +68 -0
- package/dist/entities/message-context.d.ts +9 -0
- package/dist/entities/message-write.d.ts +12 -0
- package/dist/entities/message.d.ts +6 -0
- package/dist/entities/message.js +6 -0
- package/dist/entities/store-records.d.ts +3 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/mail/cli/marks.d.ts +1 -0
- package/dist/mail/cli/marks.js +11 -4
- package/dist/mail/cli/review.js +10 -2
- package/dist/mail/findings.d.ts +55 -0
- package/dist/mail/findings.js +140 -0
- package/dist/mail/index.d.ts +2 -1
- package/dist/mail/index.js +2 -1
- package/dist/mail/marks.d.ts +10 -3
- package/dist/mail/marks.js +34 -5
- package/dist/meetings/findings-rules.d.ts +24 -0
- package/dist/meetings/findings-rules.js +136 -116
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +2 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/services/proposals/kinds/mail-findings.d.ts +8 -0
- package/dist/services/proposals/kinds/mail-findings.js +87 -0
- package/dist/services/proposals/registry.js +3 -1
- package/dist/services/proposals/types.d.ts +2 -0
- package/dist/store/decisions.d.ts +1 -1
- package/dist/store/decisions.js +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +1 -0
- package/dist/store/mail-findings.d.ts +51 -0
- package/dist/store/mail-findings.js +1 -0
- package/dist/store/path.d.ts +2 -0
- package/dist/store/path.js +21 -1
- package/dist/store/sqlite/email-activity.d.ts +8 -0
- package/dist/store/sqlite/email-activity.js +31 -5
- package/dist/store/sqlite/findings-core.d.ts +1 -0
- package/dist/store/sqlite/findings-core.js +1 -1
- package/dist/store/sqlite/mail-findings.d.ts +3 -0
- package/dist/store/sqlite/mail-findings.js +333 -0
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/memories.d.ts +4 -1
- package/dist/store/sqlite/memories.js +48 -43
- package/dist/store/sqlite/migrations.generated.js +6 -0
- package/dist/store/sqlite/open-store.d.ts +1 -1
- package/dist/store/sqlite/open-store.js +4 -1
- package/dist/store/sqlite/open.js +2 -1
- package/dist/store/sqlite/proposed-actions.js +2 -1
- package/dist/store/sqlite/tasks.d.ts +2 -0
- package/dist/store/sqlite/tasks.js +26 -24
- package/dist/store/sqlite/test-accounts.d.ts +8 -0
- package/dist/store/sqlite/test-accounts.js +31 -0
- package/dist/store/sqlite/work-tasks.d.ts +1 -0
- package/dist/store/sqlite/work-tasks.js +6 -0
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, WORK_TASK_TYPES } from "../domain/knowledge.js";
|
|
3
|
+
import { author, id, reference, text, time } from "./fields.js";
|
|
4
|
+
const evidence = v.strictObject({ reference, fingerprint: text });
|
|
5
|
+
const base = { id, evidence: v.array(evidence) };
|
|
6
|
+
const finding = v.variant("kind", [
|
|
7
|
+
v.strictObject({
|
|
8
|
+
...base,
|
|
9
|
+
kind: v.literal("task"),
|
|
10
|
+
project: v.optional(reference),
|
|
11
|
+
type: v.picklist(WORK_TASK_TYPES),
|
|
12
|
+
title: text,
|
|
13
|
+
reason: text,
|
|
14
|
+
commitment: v.picklist(["explicit", "inferred"]),
|
|
15
|
+
due_at: v.optional(time),
|
|
16
|
+
due_evidence: v.optional(v.array(reference)),
|
|
17
|
+
assignee: v.optional(reference),
|
|
18
|
+
assignee_evidence: v.optional(v.array(reference)),
|
|
19
|
+
}),
|
|
20
|
+
v.strictObject({
|
|
21
|
+
...base,
|
|
22
|
+
kind: v.literal("fact"),
|
|
23
|
+
project: v.optional(reference),
|
|
24
|
+
subject: v.optional(reference),
|
|
25
|
+
type: v.picklist(FACT_TYPES),
|
|
26
|
+
statement: text,
|
|
27
|
+
}),
|
|
28
|
+
v.strictObject({
|
|
29
|
+
...base,
|
|
30
|
+
kind: v.literal("task_update"),
|
|
31
|
+
task: reference,
|
|
32
|
+
change: v.picklist(["close", "due", "detail"]),
|
|
33
|
+
due_at: v.optional(time),
|
|
34
|
+
detail: v.optional(text),
|
|
35
|
+
reason: text,
|
|
36
|
+
}),
|
|
37
|
+
v.strictObject({
|
|
38
|
+
...base,
|
|
39
|
+
kind: v.literal("memory"),
|
|
40
|
+
memory_kind: v.picklist(MEMORY_KINDS),
|
|
41
|
+
scope: v.picklist(MEMORY_SCOPES),
|
|
42
|
+
body: text,
|
|
43
|
+
subject: v.optional(reference),
|
|
44
|
+
project: v.optional(reference),
|
|
45
|
+
}),
|
|
46
|
+
v.strictObject({ ...base, kind: v.literal("link"), from: reference, to: reference, reason: text }),
|
|
47
|
+
v.strictObject({ ...base, kind: v.literal("new_contact"), address: text, name: text, reason: v.optional(text) }),
|
|
48
|
+
]);
|
|
49
|
+
const outcome = v.strictObject({
|
|
50
|
+
id,
|
|
51
|
+
fingerprint: text,
|
|
52
|
+
status: v.picklist(["accepted", "rejected"]),
|
|
53
|
+
reference: v.nullable(reference),
|
|
54
|
+
reused: v.boolean(),
|
|
55
|
+
});
|
|
56
|
+
/** A mail findings batch: what an agent read in one mailbox's new mail, waiting for the owner. */
|
|
57
|
+
export const mailFindings = v.strictObject({
|
|
58
|
+
id,
|
|
59
|
+
account: v.strictObject({ provider: text, account: text }),
|
|
60
|
+
input: v.strictObject({ schema_version: v.literal(1), key: text, extractor: text, findings: v.array(finding) }),
|
|
61
|
+
fingerprint: text,
|
|
62
|
+
status: v.picklist(["proposed", "reviewed"]),
|
|
63
|
+
author,
|
|
64
|
+
decided_by: v.nullable(v.strictObject({ type: v.picklist(["person", "bot"]), id })),
|
|
65
|
+
decided_at: v.nullable(time),
|
|
66
|
+
previous: v.array(outcome),
|
|
67
|
+
receipt: v.nullable(v.strictObject({ preview_fingerprint: text, accepted: v.array(id), items: v.array(outcome) })),
|
|
68
|
+
});
|
|
@@ -222,6 +222,9 @@ export declare const messageThread: v.StrictObjectSchema<{
|
|
|
222
222
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
223
223
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
224
224
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
225
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
226
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
227
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
225
228
|
}, undefined>, undefined>;
|
|
226
229
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
227
230
|
}, undefined>, undefined>;
|
|
@@ -466,6 +469,9 @@ export declare const messageContext: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
466
469
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
467
470
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
468
471
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
472
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
473
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
474
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
469
475
|
}, undefined>, undefined>;
|
|
470
476
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
471
477
|
}, undefined>, v.StrictObjectSchema<{
|
|
@@ -691,6 +697,9 @@ export declare const messageContext: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
691
697
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
692
698
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
693
699
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
700
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
701
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
702
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
694
703
|
}, undefined>, undefined>;
|
|
695
704
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
696
705
|
}, undefined>, undefined>;
|
|
@@ -218,6 +218,9 @@ export declare const messageWrite: v.StrictObjectSchema<{
|
|
|
218
218
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
219
219
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
220
220
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
221
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
222
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
223
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
221
224
|
}, undefined>, undefined>;
|
|
222
225
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
223
226
|
}, undefined>;
|
|
@@ -439,6 +442,9 @@ export declare const messageWrite: v.StrictObjectSchema<{
|
|
|
439
442
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
440
443
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
441
444
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
445
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
446
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
447
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
442
448
|
}, undefined>, undefined>;
|
|
443
449
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
444
450
|
}, undefined>, undefined>, undefined>;
|
|
@@ -668,6 +674,9 @@ export declare const messageSend: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
668
674
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
669
675
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
670
676
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
677
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
678
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
679
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
671
680
|
}, undefined>, undefined>;
|
|
672
681
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
673
682
|
}, undefined>;
|
|
@@ -889,6 +898,9 @@ export declare const messageSend: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
889
898
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
890
899
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
891
900
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
901
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
902
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
903
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
892
904
|
}, undefined>, undefined>;
|
|
893
905
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
894
906
|
}, undefined>, undefined>, undefined>;
|
|
@@ -244,6 +244,12 @@ export declare const message: v.StrictObjectSchema<{
|
|
|
244
244
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
245
245
|
/** Mail `review`: the mail rule that decided how it is stored, by its id in `rules list`; `null` when none. */
|
|
246
246
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
247
|
+
/** Mail `review`: the evidence fingerprint a finding cites this email by. */
|
|
248
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
249
|
+
/** Mail `review`: no earlier stored email, in any account, came from this address. */
|
|
250
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
251
|
+
/** Mail `review`: how many identities the sender's person has; 1 is this address alone. */
|
|
252
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
247
253
|
}, undefined>, undefined>;
|
|
248
254
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
249
255
|
}, undefined>;
|
package/dist/entities/message.js
CHANGED
|
@@ -109,6 +109,12 @@ export const message = v.strictObject({
|
|
|
109
109
|
list_id: v.optional(v.nullable(text)),
|
|
110
110
|
/** Mail `review`: the mail rule that decided how it is stored, by its id in `rules list`; `null` when none. */
|
|
111
111
|
rule_id: v.optional(v.nullable(v.number())),
|
|
112
|
+
/** Mail `review`: the evidence fingerprint a finding cites this email by. */
|
|
113
|
+
fingerprint: v.optional(v.pipe(text, v.regex(/^[a-f0-9]{64}$/))),
|
|
114
|
+
/** Mail `review`: no earlier stored email, in any account, came from this address. */
|
|
115
|
+
new_sender: v.optional(v.boolean()),
|
|
116
|
+
/** Mail `review`: how many identities the sender's person has; 1 is this address alone. */
|
|
117
|
+
sender_identities: v.optional(v.number()),
|
|
112
118
|
})),
|
|
113
119
|
provider_metadata: providerMetadata,
|
|
114
120
|
});
|
|
@@ -241,6 +241,9 @@ export declare const storeExport: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
241
241
|
readonly bulk: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
242
242
|
readonly list_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
243
243
|
readonly rule_id: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
244
|
+
readonly fingerprint: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, undefined>]>, undefined>;
|
|
245
|
+
readonly new_sender: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
246
|
+
readonly sender_identities: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
244
247
|
}, undefined>, undefined>;
|
|
245
248
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
246
249
|
}, undefined>, v.StrictObjectSchema<{
|
package/dist/errors/reasons.js
CHANGED
|
@@ -44,6 +44,8 @@ const OTHERS = [
|
|
|
44
44
|
validation("meeting-findings.stale", "meeting findings changed since they were read; stage a new batch"),
|
|
45
45
|
validation("meeting-findings.dedicated-review", "review meeting findings through the dedicated findings command"),
|
|
46
46
|
validation("evidence-findings.stale", "evidence findings changed since they were read; stage a new batch"),
|
|
47
|
+
validation("mail-findings.stale", "mail findings cite mail that changed since it was read; stage a new batch"),
|
|
48
|
+
validation("mail-findings.partial", "a cited email is not stored whole yet; cite it once it is"),
|
|
47
49
|
validation("evidence-findings.dedicated-review", "review evidence findings through the dedicated findings command"),
|
|
48
50
|
validation("document.changed", "the document changed while it was read; nothing was saved"),
|
|
49
51
|
["document.not-imported", "not_found", "the document is not imported in this source account"],
|
package/dist/mail/cli/marks.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare const mailMark: v.StrictObjectSchema<{
|
|
|
14
14
|
readonly envelope: v.NumberSchema<undefined>;
|
|
15
15
|
readonly partial: v.NumberSchema<undefined>;
|
|
16
16
|
readonly draft: v.NumberSchema<undefined>;
|
|
17
|
+
readonly suspicious: v.NumberSchema<undefined>;
|
|
17
18
|
}, undefined>;
|
|
18
19
|
readonly updated_at: v.StringSchema<undefined>;
|
|
19
20
|
}, undefined>;
|
package/dist/mail/cli/marks.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from "commander";
|
|
|
3
3
|
import * as v from "valibot";
|
|
4
4
|
import { answerEntity } from "../../cli/answer-entity.js";
|
|
5
5
|
import { outputFor } from "../../cli/context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { LOCAL_WRITE, proposesOnly } from "../../cli/effects.js";
|
|
7
7
|
import { itemOf, renderAnswer } from "../../cli/envelope.js";
|
|
8
8
|
import { acknowledgeMark } from "../marks.js";
|
|
9
9
|
const iso = (ms) => (ms === null ? null : new Date(ms).toISOString());
|
|
@@ -14,7 +14,13 @@ export const mailMark = v.strictObject({
|
|
|
14
14
|
through_received_at: v.nullable(v.string()),
|
|
15
15
|
held: v.number(),
|
|
16
16
|
read: v.number(),
|
|
17
|
-
skipped: v.strictObject({
|
|
17
|
+
skipped: v.strictObject({
|
|
18
|
+
bulk: v.number(),
|
|
19
|
+
envelope: v.number(),
|
|
20
|
+
partial: v.number(),
|
|
21
|
+
draft: v.number(),
|
|
22
|
+
suspicious: v.number(),
|
|
23
|
+
}),
|
|
18
24
|
updated_at: v.string(),
|
|
19
25
|
});
|
|
20
26
|
export const markItem = (mark) => ({
|
|
@@ -28,12 +34,13 @@ export const markItem = (mark) => ({
|
|
|
28
34
|
});
|
|
29
35
|
export const markText = (mark) => `${mark.holder} through ${mark.throughId}${mark.throughAt === null ? "" : ` (${iso(mark.throughAt)})`} ` +
|
|
30
36
|
`read ${mark.read} held ${mark.held.length} skipped bulk ${mark.skipped.bulk}, envelope ${mark.skipped.envelope}, ` +
|
|
31
|
-
`partial ${mark.skipped.partial}, draft ${mark.skipped.draft}
|
|
37
|
+
`partial ${mark.skipped.partial}, draft ${mark.skipped.draft}, suspicious ${mark.skipped.suspicious} ` +
|
|
38
|
+
`updated ${iso(mark.updatedAt)}\n`;
|
|
32
39
|
/**
|
|
33
40
|
* `messages acknowledge`: a holder says it is done with what `review --holder` gave it, and its mark moves there. It
|
|
34
41
|
* only moves the holder's own bookkeeping forward, so a read-only scheduled run may do it.
|
|
35
42
|
*/
|
|
36
|
-
export const acknowledgeCommand = (mail) => answerEntity(
|
|
43
|
+
export const acknowledgeCommand = (mail) => answerEntity(proposesOnly(annotate(new Command("acknowledge"), {
|
|
37
44
|
effects: LOCAL_WRITE,
|
|
38
45
|
examples: [
|
|
39
46
|
`${mail.app.command} personal messages acknowledge --holder mail-triage --cursor 9123.1762090000000`,
|
package/dist/mail/cli/review.js
CHANGED
|
@@ -114,6 +114,13 @@ const reviewAnswer = (found, mailbox, fresh) => {
|
|
|
114
114
|
bulk: message.marks?.bulk ?? false,
|
|
115
115
|
list_id: message.marks?.listId ?? null,
|
|
116
116
|
rule_id: message.marks?.ruleId ?? null,
|
|
117
|
+
...(message.marks
|
|
118
|
+
? {
|
|
119
|
+
fingerprint: message.marks.fingerprint,
|
|
120
|
+
new_sender: message.marks.newSender,
|
|
121
|
+
sender_identities: message.marks.senderIdentities,
|
|
122
|
+
}
|
|
123
|
+
: {}),
|
|
117
124
|
},
|
|
118
125
|
})),
|
|
119
126
|
limit: Math.max(1, read.length),
|
|
@@ -202,11 +209,12 @@ export const mailReviewCommand = (mail) => annotate(new Command("review"), {
|
|
|
202
209
|
.option("--unanswered [duration]", "only threads whose newest email came to you, in To or Cc, with no answer for this long — 4h, 1d; 24h if not given")
|
|
203
210
|
.option("--waiting [duration]", "only threads where you wrote last and nobody answered for this long — 3d; 7d if not given")
|
|
204
211
|
.option("--holder <name>", "read for this holder — an agent or a pipeline — what is stored past its processing mark, by store order; " +
|
|
205
|
-
"bulk, envelopes, drafts and partial mail left out; `messages acknowledge` moves the mark")
|
|
212
|
+
"bulk, envelopes, drafts, suspicious senders and partial mail left out; `messages acknowledge` moves the mark")
|
|
206
213
|
.option("--new", "what changed since the last `review --new` of this mailbox, a point per thread — for scheduled runs")
|
|
207
214
|
.addHelpText("after", `
|
|
208
215
|
Without a date, --unanswered and --waiting look at threads active in the last ${OWED_DAYS} days.
|
|
209
|
-
Every email is shown, mail sent to many too; each item's email says bulk, list_id and rule_id, and its sender_score
|
|
216
|
+
Every email is shown, mail sent to many too; each item's email says bulk, list_id and rule_id, and its sender_score,
|
|
217
|
+
the fingerprint a finding cites it by, new_sender (no earlier email from the address) and sender_identities.
|
|
210
218
|
bulk:false leaves out threads whose newest email is mail sent to many, a list's or recorded by an envelope rule.
|
|
211
219
|
Mail newer than the last finished sync waits for the next review. Each reader keeps its own --new point: you,
|
|
212
220
|
an agent over MCP, and each scheduled run.`)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { MEMORY_KINDS, MEMORY_SCOPES } from "../domain/knowledge.js";
|
|
2
|
+
import { type FactFinding, type FindingEvidence, type TaskFinding } from "../meetings/index.js";
|
|
3
|
+
export { MAIL_FINDINGS_KIND } from "../store/mail-findings.js";
|
|
4
|
+
/** At most this many findings in one run's batch, and per sender in it. */
|
|
5
|
+
export declare const MAIL_FINDINGS_PER_RUN = 30;
|
|
6
|
+
export declare const MAIL_FINDINGS_PER_SENDER = 5;
|
|
7
|
+
interface Cited {
|
|
8
|
+
id: string;
|
|
9
|
+
evidence: FindingEvidence[];
|
|
10
|
+
}
|
|
11
|
+
export type MailTaskFinding = TaskFinding;
|
|
12
|
+
export type MailFactFinding = FactFinding;
|
|
13
|
+
export interface MailTaskUpdateFinding extends Cited {
|
|
14
|
+
kind: "task_update";
|
|
15
|
+
task: string;
|
|
16
|
+
change: "close" | "due" | "detail";
|
|
17
|
+
dueAt?: string;
|
|
18
|
+
detail?: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface MailMemoryFinding extends Cited {
|
|
22
|
+
kind: "memory";
|
|
23
|
+
memoryKind: (typeof MEMORY_KINDS)[number];
|
|
24
|
+
scope: (typeof MEMORY_SCOPES)[number];
|
|
25
|
+
body: string;
|
|
26
|
+
subject?: string;
|
|
27
|
+
project?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Address → person, person → organization, or thread → project. */
|
|
30
|
+
export interface MailLinkFinding extends Cited {
|
|
31
|
+
kind: "link";
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
export interface MailNewContactFinding extends Cited {
|
|
37
|
+
kind: "new_contact";
|
|
38
|
+
address: string;
|
|
39
|
+
name: string;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
export type MailFinding = MailTaskFinding | MailFactFinding | MailTaskUpdateFinding | MailMemoryFinding | MailLinkFinding | MailNewContactFinding;
|
|
43
|
+
export interface MailFindingsInput {
|
|
44
|
+
schemaVersion: 1;
|
|
45
|
+
/** `<holder>:<through_id>`: one batch per run of a holder over the mail it was given. */
|
|
46
|
+
key: string;
|
|
47
|
+
extractor: string;
|
|
48
|
+
findings: MailFinding[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The shape of a mail findings batch: 1–30 findings, each citing 1–8 `msg:email/…` references with fingerprints.
|
|
52
|
+
* What needs the store — the fingerprints, partial mail, the per-sender cap, the targets — the store checks.
|
|
53
|
+
*/
|
|
54
|
+
export declare const validateMailFindings: (input: MailFindingsInput) => MailFindingsInput;
|
|
55
|
+
export declare const mailFindingFingerprint: ({ id: _id, ...finding }: MailFinding) => string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { MEMORY_KINDS, MEMORY_SCOPES } from "../domain/knowledge.js";
|
|
3
|
+
import { formatLocator, parseLocator } from "../domain/locator.js";
|
|
4
|
+
import { parseReference } from "../domain/references.js";
|
|
5
|
+
import { findingFields, findingHash, findingId, findingText, meetingTimestamp, validateBatch, validateFinding, validEvidence, } from "../meetings/index.js";
|
|
6
|
+
export { MAIL_FINDINGS_KIND } from "../store/mail-findings.js";
|
|
7
|
+
/** At most this many findings in one run's batch, and per sender in it. */
|
|
8
|
+
export const MAIL_FINDINGS_PER_RUN = 30;
|
|
9
|
+
export const MAIL_FINDINGS_PER_SENDER = 5;
|
|
10
|
+
const invalid = (message) => {
|
|
11
|
+
throw new CliError("validation_error", message);
|
|
12
|
+
};
|
|
13
|
+
const emailEvidence = (raw) => {
|
|
14
|
+
try {
|
|
15
|
+
const locator = parseLocator(raw);
|
|
16
|
+
if (locator.provider === "email")
|
|
17
|
+
return formatLocator(locator);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return invalid("mail finding evidence is a msg:email/… reference");
|
|
21
|
+
};
|
|
22
|
+
const RULES = {
|
|
23
|
+
plural: "mail findings",
|
|
24
|
+
singular: "mail finding",
|
|
25
|
+
cited: "reference",
|
|
26
|
+
reference: emailEvidence,
|
|
27
|
+
anySubject: true,
|
|
28
|
+
inboxTasks: true,
|
|
29
|
+
};
|
|
30
|
+
const typeOf = (reference) => {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = parseReference(reference);
|
|
33
|
+
return parsed.type === "contact" || parsed.type === "chat" ? `${parsed.type}:${parsed.provider}` : parsed.type;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const LINKS = {
|
|
40
|
+
"contact:email": "person",
|
|
41
|
+
person: "organization",
|
|
42
|
+
"chat:email": "project",
|
|
43
|
+
};
|
|
44
|
+
const ADDRESS = /^[^\s@<>]+@[^\s@<>]+$/;
|
|
45
|
+
const one = (raw, ids) => {
|
|
46
|
+
if (!raw || typeof raw !== "object")
|
|
47
|
+
return invalid("a finding is an object");
|
|
48
|
+
if (raw.kind === "task" || raw.kind === "fact")
|
|
49
|
+
return validateFinding(raw, RULES, ids);
|
|
50
|
+
const id = findingId(raw.id, ids);
|
|
51
|
+
const evidence = validEvidence(raw.evidence, RULES);
|
|
52
|
+
const reference = (value, pattern, what) => {
|
|
53
|
+
const text = findingText(value, 400);
|
|
54
|
+
if (!pattern.test(text))
|
|
55
|
+
return invalid(`${what} is not a reference of the kind it takes: ${text}`);
|
|
56
|
+
return text;
|
|
57
|
+
};
|
|
58
|
+
switch (raw.kind) {
|
|
59
|
+
case "task_update": {
|
|
60
|
+
findingFields(raw, ["id", "kind", "task", "change", "dueAt", "detail", "reason", "evidence"], RULES.singular);
|
|
61
|
+
const task = reference(raw.task, /^task:[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/, "a task update's task");
|
|
62
|
+
const change = raw.change;
|
|
63
|
+
if (change !== "close" && change !== "due" && change !== "detail")
|
|
64
|
+
return invalid("a task update's change is close, due or detail");
|
|
65
|
+
if ((change === "due") !== (raw.dueAt !== undefined))
|
|
66
|
+
return invalid("a due change, and only it, gives due_at");
|
|
67
|
+
if ((change === "detail") !== (raw.detail !== undefined))
|
|
68
|
+
return invalid("a detail change, and only it, gives detail");
|
|
69
|
+
return {
|
|
70
|
+
id,
|
|
71
|
+
kind: "task_update",
|
|
72
|
+
task,
|
|
73
|
+
change,
|
|
74
|
+
...(raw.dueAt === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { dueAt: meetingTimestamp(raw.dueAt, "a task update's due date") }),
|
|
77
|
+
...(raw.detail === undefined ? {} : { detail: findingText(raw.detail, 2000) }),
|
|
78
|
+
reason: findingText(raw.reason, 2000),
|
|
79
|
+
evidence,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "memory": {
|
|
83
|
+
findingFields(raw, ["id", "kind", "memoryKind", "scope", "body", "subject", "project", "evidence"], RULES.singular);
|
|
84
|
+
if (!MEMORY_KINDS.includes(raw.memoryKind))
|
|
85
|
+
return invalid(`a memory's memory_kind is ${MEMORY_KINDS.join(", ")}`);
|
|
86
|
+
if (!MEMORY_SCOPES.includes(raw.scope))
|
|
87
|
+
return invalid("a memory's scope is personal or work");
|
|
88
|
+
if (raw.subject === undefined && raw.project === undefined)
|
|
89
|
+
return invalid("a memory is about a subject or a project");
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
kind: "memory",
|
|
93
|
+
memoryKind: raw.memoryKind,
|
|
94
|
+
scope: raw.scope,
|
|
95
|
+
body: findingText(raw.body, 20_000),
|
|
96
|
+
...(raw.subject === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { subject: reference(raw.subject, /^(person|organization|project):[1-9]\d*$/, "a memory's subject") }),
|
|
99
|
+
...(raw.project === undefined ? {} : { project: reference(raw.project, /^project:[1-9]\d*$/, "a project") }),
|
|
100
|
+
evidence,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case "link": {
|
|
104
|
+
findingFields(raw, ["id", "kind", "from", "to", "reason", "evidence"], RULES.singular);
|
|
105
|
+
const from = findingText(raw.from, 400);
|
|
106
|
+
const to = findingText(raw.to, 80);
|
|
107
|
+
const wanted = LINKS[typeOf(from) ?? ""];
|
|
108
|
+
if (!wanted || typeOf(to) !== wanted || !/:[1-9]\d*$/.test(to))
|
|
109
|
+
return invalid("a link goes from contact:email/<address> to person:, person: to organization:, or chat:email/… to project:");
|
|
110
|
+
return { id, kind: "link", from, to, reason: findingText(raw.reason, 2000), evidence };
|
|
111
|
+
}
|
|
112
|
+
case "new_contact": {
|
|
113
|
+
findingFields(raw, ["id", "kind", "address", "name", "reason", "evidence"], RULES.singular);
|
|
114
|
+
const address = findingText(raw.address, 320).toLowerCase();
|
|
115
|
+
if (!ADDRESS.test(address))
|
|
116
|
+
return invalid("a new contact's address is an email address");
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
kind: "new_contact",
|
|
120
|
+
address,
|
|
121
|
+
name: findingText(raw.name, 200),
|
|
122
|
+
...(raw.reason === undefined ? {} : { reason: findingText(raw.reason, 2000) }),
|
|
123
|
+
evidence,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
return invalid("a mail finding is a task, task_update, fact, memory, link or new_contact");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* The shape of a mail findings batch: 1–30 findings, each citing 1–8 `msg:email/…` references with fingerprints.
|
|
132
|
+
* What needs the store — the fingerprints, partial mail, the per-sender cap, the targets — the store checks.
|
|
133
|
+
*/
|
|
134
|
+
export const validateMailFindings = (input) => {
|
|
135
|
+
const batch = validateBatch(input, RULES, MAIL_FINDINGS_PER_RUN, one);
|
|
136
|
+
if (!/^[a-z0-9][a-z0-9-]{0,39}:[1-9]\d*$/.test(batch.key))
|
|
137
|
+
return invalid("a mail findings key is <holder>:<through_id>, such as mail-triage:9123456");
|
|
138
|
+
return batch;
|
|
139
|
+
};
|
|
140
|
+
export const mailFindingFingerprint = ({ id: _id, ...finding }) => findingHash(finding);
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export { mailAccountKey } from "./account-key.js";
|
|
2
2
|
export { BULK, bulkBox } from "./bulk.js";
|
|
3
3
|
export { factsOf } from "./facts.js";
|
|
4
|
+
export { MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, type MailFinding, type MailFindingsInput, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
|
|
4
5
|
export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
|
|
5
6
|
export { dropMissing, type Gone, tooManyGone } from "./gone.js";
|
|
6
7
|
export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHeadersOf } from "./headers.js";
|
|
7
8
|
export { type MailIdsInput, mailIds } from "./ids.js";
|
|
8
9
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
9
|
-
export { acknowledgeMark, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, type MailMark, type MarkRead, markOf, marksOf, readPastMark, type SkipReason, } from "./marks.js";
|
|
10
|
+
export { acknowledgeMark, cursorOf, HOLD_PARTIAL_MS, HOLDER, holderHasMail, MARK_BATCH, type MailMark, type MarkRead, markOf, marksOf, readPastMark, type SkipReason, } from "./marks.js";
|
|
10
11
|
export { type MailTurn, mailTurn } from "./owed.js";
|
|
11
12
|
export { type Address, htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, type ParsedMail, parseMail, } from "./parse.js";
|
|
12
13
|
export { PARTIAL, type PartialOutcome } from "./partial.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export { mailAccountKey } from "./account-key.js";
|
|
2
2
|
export { BULK, bulkBox } from "./bulk.js";
|
|
3
3
|
export { factsOf } from "./facts.js";
|
|
4
|
+
export { MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
|
|
4
5
|
export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
|
|
5
6
|
export { dropMissing, tooManyGone } from "./gone.js";
|
|
6
7
|
export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
|
7
8
|
export { mailIds } from "./ids.js";
|
|
8
9
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
9
|
-
export { acknowledgeMark, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, markOf, marksOf, readPastMark, } from "./marks.js";
|
|
10
|
+
export { acknowledgeMark, cursorOf, HOLD_PARTIAL_MS, HOLDER, holderHasMail, MARK_BATCH, markOf, marksOf, readPastMark, } from "./marks.js";
|
|
10
11
|
export { mailTurn } from "./owed.js";
|
|
11
12
|
export { htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, parseMail, } from "./parse.js";
|
|
12
13
|
export { PARTIAL } from "./partial.js";
|
package/dist/mail/marks.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const HOLDER: RegExp;
|
|
|
5
5
|
export declare const MARK_BATCH = 200;
|
|
6
6
|
/** A partial email waits this long to be stored whole, then is skipped. */
|
|
7
7
|
export declare const HOLD_PARTIAL_MS: number;
|
|
8
|
-
export type SkipReason = "bulk" | "envelope" | "partial" | "draft";
|
|
8
|
+
export type SkipReason = "bulk" | "envelope" | "partial" | "draft" | "suspicious";
|
|
9
9
|
/**
|
|
10
10
|
* How far one holder — an agent, a pipeline — has processed an account's mail, in the shared store (`sync_cursors`,
|
|
11
11
|
* key `consumer:<holder>`). It counts by the store's insert order (`emails.id`), so mail stored late, older than what
|
|
@@ -28,7 +28,7 @@ export declare const marksOf: (store: MessageStore, mailbox: AccountKey) => Prom
|
|
|
28
28
|
/** The mark's state, as a cursor `acknowledge` takes back: the last id read, and when it was read. */
|
|
29
29
|
export declare const cursorOf: (throughId: number, readAt: number) => string;
|
|
30
30
|
export interface MarkRead {
|
|
31
|
-
/** The emails for the holder, oldest first; bulk, envelopes, drafts and partial mail left out. */
|
|
31
|
+
/** The emails for the holder, oldest first; bulk, envelopes, drafts, suspicious senders and partial mail left out. */
|
|
32
32
|
emails: StoredHit[];
|
|
33
33
|
/** What `acknowledge --cursor` takes once the holder is done with these. */
|
|
34
34
|
cursor: string;
|
|
@@ -44,10 +44,17 @@ export interface MarkRead {
|
|
|
44
44
|
* only; the mark moves when the holder acknowledges. A holder with no mark starts from now: its first read is
|
|
45
45
|
* empty and gives the cursor of the newest stored email.
|
|
46
46
|
*/
|
|
47
|
-
export declare const readPastMark: (store: MessageStore, mailbox: AccountKey, holder: string,
|
|
47
|
+
export declare const readPastMark: (store: MessageStore, mailbox: AccountKey, holder: string, options?: {
|
|
48
48
|
now?: number;
|
|
49
49
|
limit?: number;
|
|
50
50
|
}) => Promise<MarkRead>;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the holder has mail to read in any stored mailbox — what a schedule asks before it starts the holder's
|
|
53
|
+
* agent, so a run with nothing new costs no model call. Mail that is only skipped (bulk, envelopes, drafts,
|
|
54
|
+
* suspicious senders) moves the holder's mark here, as the agent would have acknowledged it, or it would wait
|
|
55
|
+
* forever behind the mark. A holder with no mark yet has mail to read: its first run sets the mark.
|
|
56
|
+
*/
|
|
57
|
+
export declare const holderHasMail: (store: MessageStore, holder: string, now?: number) => Promise<boolean>;
|
|
51
58
|
/**
|
|
52
59
|
* Moves the holder's mark to the cursor its read gave, never back. An email whole when it was read counts read; one
|
|
53
60
|
* still partial, or changed after the read (it may have become whole since), is held and comes with the next read.
|
package/dist/mail/marks.js
CHANGED
|
@@ -8,7 +8,7 @@ export const HOLD_PARTIAL_MS = 7 * 86_400_000;
|
|
|
8
8
|
/** Held emails kept on one mark at most; past it the oldest are skipped as partial. */
|
|
9
9
|
const MOST_HELD = 500;
|
|
10
10
|
const PREFIX = "consumer:";
|
|
11
|
-
const noneSkipped = () => ({ bulk: 0, envelope: 0, partial: 0, draft: 0 });
|
|
11
|
+
const noneSkipped = () => ({ bulk: 0, envelope: 0, partial: 0, draft: 0, suspicious: 0 });
|
|
12
12
|
const keyOf = (holder) => {
|
|
13
13
|
if (!HOLDER.test(holder))
|
|
14
14
|
throw new CliError("validation_error", `a holder is lower-case letters, digits and dashes, not "${holder}"`);
|
|
@@ -59,6 +59,8 @@ const verdict = (email, now) => {
|
|
|
59
59
|
return "envelope";
|
|
60
60
|
if (email.bulk)
|
|
61
61
|
return "bulk";
|
|
62
|
+
if (email.demoted)
|
|
63
|
+
return "suspicious";
|
|
62
64
|
if (email.partial)
|
|
63
65
|
return now - email.storedAt < HOLD_PARTIAL_MS ? "held" : "partial";
|
|
64
66
|
return "read";
|
|
@@ -76,10 +78,15 @@ const parseCursor = (cursor) => {
|
|
|
76
78
|
* only; the mark moves when the holder acknowledges. A holder with no mark starts from now: its first read is
|
|
77
79
|
* empty and gives the cursor of the newest stored email.
|
|
78
80
|
*/
|
|
79
|
-
export const readPastMark = async (store, mailbox, holder,
|
|
81
|
+
export const readPastMark = async (store, mailbox, holder, options = {}) => {
|
|
82
|
+
const { wanted, ...read } = await examine(store, mailbox, holder, options);
|
|
83
|
+
return { emails: await store.mail.emailHits(wanted), ...read };
|
|
84
|
+
};
|
|
85
|
+
/** `readPastMark` without the emails' text: the store ids it would read, by their verdicts. */
|
|
86
|
+
const examine = async (store, mailbox, holder, { now = Date.now(), limit = MARK_BATCH }) => {
|
|
80
87
|
const mark = await markOf(store, mailbox, holder);
|
|
81
88
|
const account = await accountOf(store, mailbox);
|
|
82
|
-
const empty = {
|
|
89
|
+
const empty = { wanted: [], more: false, held: 0, skipped: noneSkipped(), mark };
|
|
83
90
|
if (!account)
|
|
84
91
|
return { ...empty, cursor: cursorOf(0, now) };
|
|
85
92
|
if (!mark)
|
|
@@ -105,9 +112,8 @@ export const readPastMark = async (store, mailbox, holder, { now = Date.now(), l
|
|
|
105
112
|
else if (email.id > mark.throughId)
|
|
106
113
|
skipped[said] += 1;
|
|
107
114
|
}
|
|
108
|
-
const emails = await store.mail.emailHits(wanted.sort((a, b) => a - b));
|
|
109
115
|
return {
|
|
110
|
-
|
|
116
|
+
wanted: wanted.sort((a, b) => a - b),
|
|
111
117
|
cursor: cursorOf(batch.at(-1)?.id ?? mark.throughId, now),
|
|
112
118
|
more: after.length > batch.length,
|
|
113
119
|
held,
|
|
@@ -115,6 +121,29 @@ export const readPastMark = async (store, mailbox, holder, { now = Date.now(), l
|
|
|
115
121
|
mark,
|
|
116
122
|
};
|
|
117
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Whether the holder has mail to read in any stored mailbox — what a schedule asks before it starts the holder's
|
|
126
|
+
* agent, so a run with nothing new costs no model call. Mail that is only skipped (bulk, envelopes, drafts,
|
|
127
|
+
* suspicious senders) moves the holder's mark here, as the agent would have acknowledged it, or it would wait
|
|
128
|
+
* forever behind the mark. A holder with no mark yet has mail to read: its first run sets the mark.
|
|
129
|
+
*/
|
|
130
|
+
export const holderHasMail = async (store, holder, now = Date.now()) => {
|
|
131
|
+
for (const mailbox of await store.accounts.accounts()) {
|
|
132
|
+
if (mailbox.provider !== "email")
|
|
133
|
+
continue;
|
|
134
|
+
for (;;) {
|
|
135
|
+
const read = await examine(store, mailbox, holder, { now });
|
|
136
|
+
if (!read.mark || read.wanted.length > 0)
|
|
137
|
+
return true;
|
|
138
|
+
if (read.cursor === cursorOf(read.mark.throughId, now))
|
|
139
|
+
break;
|
|
140
|
+
await acknowledgeMark(store, mailbox, holder, read.cursor, now);
|
|
141
|
+
if (!read.more)
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
};
|
|
118
147
|
/**
|
|
119
148
|
* Moves the holder's mark to the cursor its read gave, never back. An email whole when it was read counts read; one
|
|
120
149
|
* still partial, or changed after the read (it may have become whole since), is held and comes with the next read.
|