@wirecat/cli-core 0.580.0 → 0.581.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 CHANGED
@@ -5,6 +5,24 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.581.0 — 05.11.2026
11
+
12
+ ### Added
13
+
14
+ - **`review --holder` gives the findings key.** A host that mounts its findings command (`MailMount.findings`)
15
+ gets a `mail-findings.add` step with `key` before the acknowledge step, so an agent copies the key instead of
16
+ building it from the cursor. The text answer names it too.
17
+
18
+ ### Fixed
19
+
20
+ - **A run after `messages reset` can stage its findings.** The reset counts on the mark (`resets`), and the findings
21
+ key of a mark moved back is `<holder>:<through_id>-r<resets>`, such as `mail-triage:277352-r1`. The runs after a
22
+ reset reached the through id of a batch staged before it and were refused (`mail-findings.key-used`), so their
23
+ mark stayed. Keys of marks never moved back keep their shape, and the same review still gives the same key, so
24
+ a retry of one run stages nothing twice.
25
+
8
26
  ## 0.580.0 — 05.11.2026
9
27
 
10
28
  ### Added
@@ -15,6 +15,8 @@ export interface MailMount {
15
15
  acknowledge?: string;
16
16
  /** `null`: the host mounts no thread read, so a review offers none. */
17
17
  conversation?: string | null;
18
+ /** The host's command that stages a findings batch with `--key`; `review --holder` then gives that key as a step. */
19
+ findings?: string;
18
20
  /** In snake_case, as a step names it: memo's `source`, with `email:<account>`. */
19
21
  mailbox?: {
20
22
  option: string;
@@ -1,7 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { defineQuery } from "@wirecat/cli-base/query";
4
- import { defineSteps, stepOf } from "@wirecat/cli-base/standards";
4
+ import { defineSteps, MAX_NEXT_STEPS, stepOf, } from "@wirecat/cli-base/standards";
5
5
  import { Command } from "commander";
6
6
  import { environmentOf, outputFor } from "../../cli/context.js";
7
7
  import { LOCAL_READ } from "../../cli/effects.js";
@@ -36,7 +36,18 @@ export const MAIL_REVIEW_QUERY = defineQuery({
36
36
  /** The mail review's steps where a host mounted them; a host lists them beside its own so its step checks know them. */
37
37
  export const mailReviewSteps = (mount = {}) => {
38
38
  const mailbox = mount.mailbox ? [{ name: mount.mailbox.option, required: true }] : [];
39
+ const findings = mount.findings
40
+ ? [
41
+ {
42
+ id: "mail-findings.add",
43
+ command: mount.findings,
44
+ args: [{ name: "key", required: true }, ...mailbox],
45
+ why: "stage what these emails ask under this key, before acknowledging them",
46
+ },
47
+ ]
48
+ : [];
39
49
  return defineSteps([
50
+ ...findings,
40
51
  {
41
52
  id: "mail-messages.acknowledge",
42
53
  command: mount.acknowledge ?? "messages acknowledge",
@@ -197,7 +208,12 @@ const holderReview = async (command, mail, holder, query) => {
197
208
  const step = stepsFor(mail.mount, mailbox);
198
209
  const answer = reviewAnswer(found, mailbox, true, step);
199
210
  const skipped = Object.entries(read.skipped).filter(([, count]) => count > 0);
200
- answer.next = [...step("mail-messages.acknowledge", { holder, cursor: read.cursor }), ...(answer.next ?? [])];
211
+ const stage = read.findingsKey === undefined ? [] : step("mail-findings.add", { key: read.findingsKey });
212
+ answer.next = [
213
+ ...stage,
214
+ ...step("mail-messages.acknowledge", { holder, cursor: read.cursor }),
215
+ ...(answer.next ?? []),
216
+ ].slice(0, MAX_NEXT_STEPS);
201
217
  answer.missing = [
202
218
  ...(answer.missing ?? []),
203
219
  ...(read.held > 0 ? [{ field: "items", why: `${read.held} partial emails held until they are whole` }] : []),
@@ -217,7 +233,7 @@ const holderReview = async (command, mail, holder, query) => {
217
233
  const output = outputFor(command);
218
234
  renderAnswer(command, output, answer, () => {
219
235
  output.renderer.note(read.mark
220
- ? `${holder}: past ${read.mark.throughId}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
236
+ ? `${holder}: past ${read.mark.throughId}; findings key ${read.findingsKey}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
221
237
  : `${holder} has no mark yet: it starts from now — acknowledge with --cursor ${read.cursor}`);
222
238
  return emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id), hit.excerptOnly)}\n`).join("");
223
239
  });
@@ -131,7 +131,10 @@ export interface MailOrderUpdateFinding extends Cited {
131
131
  export type MailFinding = MailTaskFinding | MailFactFinding | MailTaskUpdateFinding | MailMemoryFinding | MailLinkFinding | MailNewContactFinding | MailInvoiceFinding | MailPaymentFinding | MailOrderFinding | MailOrderUpdateFinding;
132
132
  export interface MailFindingsInput {
133
133
  schemaVersion: 1;
134
- /** `<holder>:<through_id>`: one batch per run of a holder over the mail it was given. */
134
+ /**
135
+ * `<holder>:<through_id>`, with `-r<resets>` after the owner moved the mark back: one batch per run of a holder over
136
+ * the mail it was given. `review --holder` gives it.
137
+ */
135
138
  key: string;
136
139
  extractor: string;
137
140
  findings: MailFinding[];
@@ -347,8 +347,9 @@ const one = (raw, ids) => {
347
347
  * The shape of a mail findings batch: 1–30 findings, each citing 1–8 `msg:email/…` references with fingerprints.
348
348
  * What needs the store — the fingerprints, partial mail, the per-sender cap, the targets — the store checks.
349
349
  */
350
- const KEY = /^[a-z0-9][a-z0-9-]{0,39}:[1-9]\d*$/;
351
- const KEY_SHAPE = "a mail findings key is <holder>:<through_id>, such as mail-triage:9123456";
350
+ const KEY = /^[a-z0-9][a-z0-9-]{0,39}:[1-9]\d*(-r[1-9]\d*)?$/;
351
+ const KEY_SHAPE = "a mail findings key is the one `review --holder` gave: <holder>:<through_id>, and -r<resets> after a reset, " +
352
+ "such as mail-triage:9123456 or mail-triage:9123456-r1";
352
353
  export const validateMailFindings = (input) => {
353
354
  const batch = validateBatch(input, RULES, MAIL_FINDINGS_PER_RUN, one);
354
355
  if (!KEY.test(batch.key))
@@ -22,6 +22,8 @@ export interface MailMark {
22
22
  suspicious: number[];
23
23
  read: number;
24
24
  skipped: Record<SkipReason, number>;
25
+ /** How often the owner moved the mark back: a findings key names it, so a run after a reset never reuses one. */
26
+ resets: number;
25
27
  /** ms. */
26
28
  updatedAt: number;
27
29
  }
@@ -30,6 +32,12 @@ export declare const markOf: (store: MessageStore, mailbox: AccountKey, holder:
30
32
  export declare const marksOf: (store: MessageStore, mailbox: AccountKey) => Promise<MailMark[]>;
31
33
  /** The mark's state, as a cursor `acknowledge` takes back: the last id read, and when it was read. */
32
34
  export declare const cursorOf: (throughId: number, readAt: number) => string;
35
+ /**
36
+ * The findings key of one run: `<holder>:<through_id>`, and `-r<resets>` once the owner moved the mark back, so the
37
+ * runs after a reset never reach a key a batch staged before it holds. The same review gives the same key: a retry
38
+ * of one run stages nothing twice.
39
+ */
40
+ export declare const findingsKeyOf: (holder: string, throughId: number, resets: number) => string;
33
41
  /** A bulk email that is not surely a newsletter: the holder gets its subject and first lines only. */
34
42
  export interface BulkExcerpt {
35
43
  email: StoredHit;
@@ -45,6 +53,8 @@ export interface MarkRead {
45
53
  excerpts: BulkExcerpt[];
46
54
  /** What `acknowledge --cursor` takes once the holder is done with these. */
47
55
  cursor: string;
56
+ /** The key a findings batch over these emails takes; `undefined` while nothing is stored to read. */
57
+ findingsKey: string | undefined;
48
58
  /** More is stored after this batch: read again after acknowledging. */
49
59
  more: boolean;
50
60
  held: number;
@@ -39,6 +39,7 @@ const parsed = (holder, value, at) => {
39
39
  suspicious: Array.isArray(saved.suspicious) ? saved.suspicious.filter((id) => typeof id === "number") : [],
40
40
  read: saved.read ?? 0,
41
41
  skipped: { ...noneSkipped(), ...saved.skipped },
42
+ resets: typeof saved.resets === "number" ? saved.resets : 0,
42
43
  updatedAt: at,
43
44
  };
44
45
  }
@@ -80,6 +81,12 @@ const verdict = (email, now) => {
80
81
  };
81
82
  /** The mark's state, as a cursor `acknowledge` takes back: the last id read, and when it was read. */
82
83
  export const cursorOf = (throughId, readAt) => `${throughId}.${readAt}`;
84
+ /**
85
+ * The findings key of one run: `<holder>:<through_id>`, and `-r<resets>` once the owner moved the mark back, so the
86
+ * runs after a reset never reach a key a batch staged before it holds. The same review gives the same key: a retry
87
+ * of one run stages nothing twice.
88
+ */
89
+ export const findingsKeyOf = (holder, throughId, resets) => `${holder}:${throughId}${resets > 0 ? `-r${resets}` : ""}`;
83
90
  const parseCursor = (cursor) => {
84
91
  const match = /^(\d+)\.(\d+)$/.exec(cursor.trim());
85
92
  if (!match)
@@ -140,10 +147,14 @@ const examine = async (store, mailbox, holder, { now = Date.now(), limit = MARK_
140
147
  skipped: noneSkipped(),
141
148
  mark: saved,
142
149
  };
150
+ const keyed = (throughId) => ({
151
+ cursor: cursorOf(throughId, now),
152
+ findingsKey: throughId > 0 ? findingsKeyOf(holder, throughId, saved?.resets ?? 0) : undefined,
153
+ });
143
154
  if (!account)
144
- return { ...empty, cursor: cursorOf(0, now) };
155
+ return { ...empty, ...keyed(0) };
145
156
  if (!saved)
146
- return { ...empty, cursor: cursorOf(await store.mail.newestEmailId(account.id), now) };
157
+ return { ...empty, ...keyed(await store.mail.newestEmailId(account.id)) };
147
158
  const mark = after === undefined ? saved : { ...saved, throughId: after, held: [], suspicious: [] };
148
159
  const found = await store.mail.emailsAfter(account.id, mark.throughId, limit + 1, [...mark.held, ...mark.suspicious]);
149
160
  // Mail a running sync stored is read once that sync ended: it files bulk mail only at its end.
@@ -179,7 +190,7 @@ const examine = async (store, mailbox, holder, { now = Date.now(), limit = MARK_
179
190
  wanted: wanted.sort((a, b) => a - b),
180
191
  sorted: new Map([...sorted].filter(([, one]) => one.verdict === "excerpt")),
181
192
  oldestStoredAt,
182
- cursor: cursorOf(batch.at(-1)?.id ?? mark.throughId, now),
193
+ ...keyed(batch.at(-1)?.id ?? mark.throughId),
183
194
  more: newer.length > batch.length,
184
195
  held,
185
196
  skipped,
@@ -312,6 +323,7 @@ export const acknowledgeMark = async (store, mailbox, holder, cursor, now = Date
312
323
  suspicious: [...new Set(suspicious)].sort((a, b) => a - b).slice(-MOST_HELD),
313
324
  read,
314
325
  skipped,
326
+ resets: mark?.resets ?? 0,
315
327
  };
316
328
  await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
317
329
  return { holder, ...next, updatedAt: now };
@@ -397,6 +409,7 @@ export const rewindMark = async (store, mailbox, holder, to, { now = Date.now(),
397
409
  suspicious: mark.suspicious.filter((id) => id <= through),
398
410
  read: mark.read,
399
411
  skipped: mark.skipped,
412
+ resets: mark.resets + 1,
400
413
  };
401
414
  if (!dry)
402
415
  await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.580.0",
3
+ "version": "0.581.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",