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