@wirecat/cli-core 0.494.0 → 0.495.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
@@ -7,6 +7,20 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.495.0 — 31.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **`gatherProjectEvidence` says what the store holds of each linked chat, and which ones the run never reached.** A new
15
+ optional port `coverage(account, chatIds)` (pass `chatCoverage` over the store) fills each `linkedChats` row with
16
+ `title` (the chat row's, also for a chat with no stored message), `completeness` and `held`; without it they are `null` and `completeness` stays `"unknown"`. The top-level
17
+ `completeness` is `partial` when any linked chat is `partial`, `unknown` when none is but one is `unknown` (a chat the
18
+ store has no row for counts), and `complete` only when every linked chat in scope is. `complete` keeps its meaning,
19
+ "the planned reads finished"; a host's `meta.complete` is `complete && completeness === "complete"`. A linked chat
20
+ none of whose reads has started is `state: "pending"` and has no `items` or `capped`, where it said `in-scope` with
21
+ `items: 0, capped: false`. `ChatCoverage.title` now comes from the chat row when the chat has no message, through the
22
+ new `store.chats.chatTitle`.
23
+
10
24
  ## 0.494.0 — 31.10.2026
11
25
 
12
26
  ### Security
@@ -3,7 +3,7 @@ import type { AccountKey, ChatCompleteness, MessageStore } from "../store/store.
3
3
  /** What the store holds of one chat, and how far that goes: never message text. */
4
4
  export interface ChatCoverage {
5
5
  chatId: Id;
6
- /** `null` while the store holds no message of the chat: titles come with the stats. */
6
+ /** The chat row's title; `null` when the store has no row for the chat or the row has none. */
7
7
  title: string | null;
8
8
  completeness: ChatCompleteness;
9
9
  held: {
@@ -9,6 +9,7 @@ export const chatCoverage = async (store, account, chatIds) => {
9
9
  const found = new Map((await store.chats.chatCompleteness(account, ids)).map((one) => [one.chatId, one]));
10
10
  return Promise.all(ids.map(async (chatId) => {
11
11
  const [stats] = await store.chats.chatStats(account, chatId);
12
+ const title = stats?.title ?? (await store.chats.chatTitle(account, chatId));
12
13
  const completeness = found.get(chatId) ?? {
13
14
  chatId,
14
15
  state: "unknown",
@@ -19,7 +20,7 @@ export const chatCoverage = async (store, account, chatIds) => {
19
20
  };
20
21
  return {
21
22
  chatId,
22
- title: stats?.title ?? null,
23
+ title,
23
24
  completeness,
24
25
  held: { messages: stats?.messages ?? 0, oldestAt: stats?.oldestAt ?? null, newestAt: stats?.newestAt ?? null },
25
26
  };
@@ -1,4 +1,6 @@
1
1
  import type { ContextEvidenceItem, ContextEvidenceReader, ContextIndex, ContextSource } from "../context/evidence.js";
2
+ import type { AccountKey } from "../store/types.js";
3
+ import type { ChatCoverage } from "./chat-coverage.js";
2
4
  import { type ProjectNeighbour, type ProjectNeighbourReader } from "./project-gathering-around.js";
3
5
  import { type ProjectFetchState, type ProjectGatheringRequest, type ProjectLane } from "./project-gathering-request.js";
4
6
  /** A connector's history fetch into the store; the store reads that follow find what it saved. */
@@ -18,6 +20,8 @@ export interface ProjectGatheringPorts {
18
20
  fetcher?: ProjectSourceFetcher;
19
21
  index?: ContextIndex;
20
22
  neighbours?: ProjectNeighbourReader;
23
+ /** What the store holds of the named chats of one account, e.g. `chatCoverage` over the store. */
24
+ coverage?: (account: AccountKey, chatIds: string[]) => Promise<ChatCoverage[]>;
21
25
  now?: () => number;
22
26
  }
23
27
  export interface ProjectIndexCatchUp {
@@ -88,9 +92,16 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
88
92
  ref: string;
89
93
  source: number | null;
90
94
  lane: string | null;
91
- state: "in-scope" | "out-of-scope" | "unavailable";
92
- items: number;
93
- capped: boolean;
95
+ state: "in-scope" | "out-of-scope" | "pending" | "unavailable";
96
+ title: string | null;
97
+ completeness: import("../store/exports.js").ChatCompleteness | null;
98
+ held: {
99
+ messages: number;
100
+ oldestAt: string | null;
101
+ newestAt: string | null;
102
+ } | null;
103
+ items?: number | undefined;
104
+ capped?: boolean | undefined;
94
105
  }[];
95
106
  fetch: {
96
107
  state: ProjectFetchState;
@@ -136,7 +147,7 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
136
147
  };
137
148
  stopped: ProjectGatheringStop;
138
149
  complete: boolean;
139
- completeness: "unknown";
150
+ completeness: "complete" | "partial" | "unknown";
140
151
  checkpoint: string | null;
141
152
  }>;
142
153
  export type ProjectGathering = Awaited<ReturnType<typeof gatherProjectEvidence>>;
@@ -264,6 +264,20 @@ export const gatherProjectEvidence = async (ports, request) => {
264
264
  },
265
265
  }, (ref) => byRef.has(ref) || seen.has(seenKey(ref)), signal);
266
266
  const people = peopleOf(items, senderOf);
267
+ const coverageOf = new Map();
268
+ if (ports.coverage) {
269
+ const byAccount = new Map();
270
+ for (const { provider, account, chat, source } of plan.linkedChats) {
271
+ if (source === null)
272
+ continue;
273
+ const group = byAccount.get(JSON.stringify([provider, account])) ?? { account: { provider, account }, chats: [] };
274
+ group.chats.push(chat);
275
+ byAccount.set(JSON.stringify([provider, account]), group);
276
+ }
277
+ for (const [key, { account, chats }] of byAccount)
278
+ for (const row of await ports.coverage(account, chats))
279
+ coverageOf.set(`${key}${row.chatId}`, row);
280
+ }
267
281
  const totals = {
268
282
  reads: (previous?.totals.reads ?? 0) + usage.reads,
269
283
  readBytes: (previous?.totals.readBytes ?? 0) + usage.readBytes,
@@ -284,6 +298,39 @@ export const gatherProjectEvidence = async (ports, request) => {
284
298
  seen: [...seen],
285
299
  totals,
286
300
  };
301
+ const linkedRows = plan.linkedChats.map(({ provider, account, chat, ref, source, lane }) => {
302
+ const laneUnits = units.flatMap((unit, index) => (unit.lane.id === lane ? [progress[index]] : []));
303
+ const state = source === null
304
+ ? "out-of-scope"
305
+ : laneUnits.every((u) => u?.state === "unavailable")
306
+ ? "unavailable"
307
+ : laneUnits.every((u) => u?.state === "pending")
308
+ ? "pending"
309
+ : "in-scope";
310
+ const row = coverageOf.get(`${JSON.stringify([provider, account])}${chat}`);
311
+ return {
312
+ provider,
313
+ account,
314
+ chat,
315
+ ref,
316
+ source,
317
+ lane,
318
+ state,
319
+ title: row?.title ?? null,
320
+ completeness: row?.completeness ?? null,
321
+ held: row?.held ?? null,
322
+ ...(state === "pending" ? {} : { items: lane === null ? 0 : (linkedTaken.get(lane) ?? 0) }),
323
+ ...(state === "pending" ? {} : { capped: lane !== null && capped.has(lane) }),
324
+ };
325
+ });
326
+ const states = linkedRows.flatMap((row) => (row.completeness ? [row.completeness.state] : []));
327
+ const completeness = !states.length
328
+ ? "unknown"
329
+ : states.includes("partial")
330
+ ? "partial"
331
+ : states.includes("unknown") || states.length < linkedRows.filter((row) => row.source !== null).length
332
+ ? "unknown"
333
+ : "complete";
287
334
  return {
288
335
  schemaVersion: 1,
289
336
  runKey: plan.runKey,
@@ -300,21 +347,7 @@ export const gatherProjectEvidence = async (ports, request) => {
300
347
  },
301
348
  scope: { origin: plan.scope, sources: plan.sources },
302
349
  lanes: plan.lanes,
303
- linkedChats: plan.linkedChats.map(({ provider, account, chat, ref, source, lane }) => ({
304
- provider,
305
- account,
306
- chat,
307
- ref,
308
- source,
309
- lane,
310
- state: source === null
311
- ? "out-of-scope"
312
- : units.every((unit, index) => unit.lane.id !== lane || progress[index]?.state === "unavailable")
313
- ? "unavailable"
314
- : "in-scope",
315
- items: lane === null ? 0 : (linkedTaken.get(lane) ?? 0),
316
- capped: lane !== null && capped.has(lane),
317
- })),
350
+ linkedChats: linkedRows,
318
351
  fetch: fetched.map((f, index) => ({ source: index, ...f })),
319
352
  index: indexed,
320
353
  units: units.map((unit, index) => ({
@@ -335,7 +368,7 @@ export const gatherProjectEvidence = async (ports, request) => {
335
368
  totals,
336
369
  stopped,
337
370
  complete: !resumable,
338
- completeness: "unknown",
371
+ completeness,
339
372
  checkpoint: resumable ? encodeCheckpoint(checkpoint) : null,
340
373
  };
341
374
  };
@@ -66,6 +66,8 @@ export interface ChatsStore {
66
66
  renamedChatId(key: AccountKey, chatId: Id): Promise<Id | undefined>;
67
67
  /** A stored chat's kind, with or without messages; `undefined` when the store does not know the chat. */
68
68
  chatKind(key: AccountKey, chatId: Id): Promise<ChatKind | undefined>;
69
+ /** A stored chat's title, with or without messages; `null` when it has none or the store does not know the chat. */
70
+ chatTitle(key: AccountKey, chatId: Id): Promise<string | null>;
69
71
  /** Per chat that spoke since `since` (ms): its newest message and who sent it when stored, never the text. */
70
72
  /** `where` is a `tasks agenda` query compiled over `AGENDA_COLUMNS`. */
71
73
  chatActivity(key: AccountKey, since: number, where?: Where): Promise<{
@@ -94,6 +94,15 @@ export const chatsStoreOver = (context) => {
94
94
  .get(accountKey, chatId);
95
95
  return row ? String(row.kind) : undefined;
96
96
  },
97
+ chatTitle: async (key, chatId) => {
98
+ const accountKey = findAccountPk(key);
99
+ if (accountKey === undefined)
100
+ return null;
101
+ const row = context.database
102
+ .prepare("SELECT title FROM chats WHERE account_id = ? AND external_id = ?")
103
+ .get(accountKey, chatId);
104
+ return row?.title == null ? null : String(row.title);
105
+ },
97
106
  chatActivity: async (key, since, where) => {
98
107
  const accountKey = findAccountPk(key);
99
108
  return accountKey === undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.494.0",
3
+ "version": "0.495.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",