@wirecat/cli-core 0.359.0 → 0.360.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,18 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.360.0 — 19.10.2026
11
+
12
+ ### Added
13
+
14
+ - **Adapter test authors can shorten a connection's close wait and re-key pauses.** The run environment takes
15
+ `closeWaitMs` (how long a connection's close waits on its saves and passive listens, 5 s by default) and
16
+ `rekeyPacing` (`{ lockMs, pauseMs }` for chat re-keys, 50 ms and 25 ms by default). Commands, the shared services and
17
+ the MCP server's tool calls pass them on; a command `serve` runs for a client through its relay keeps the
18
+ defaults. A test through `run()` no longer pays the real waits. Only code
19
+ that builds the environment can set them; no flag, variable or MCP call does
20
+ ([ADAPTERS.md](docs/dev/ADAPTERS.md#running-the-contract-cases)).
21
+
10
22
  ## 0.359.0 — 19.10.2026
11
23
 
12
24
  ### Fixed
@@ -35,6 +35,13 @@ export interface BaseEnvironment {
35
35
  recognizer?: OpenRecognizer;
36
36
  /** How long `store restore` and `store reset` wait on another connection's write, 1 s unless a test hands one in. */
37
37
  storeLockWaitMs?: number;
38
+ /** How long a connection's close waits on its saves and passive listens, 5 s unless a test hands one in. */
39
+ closeWaitMs?: number;
40
+ /** How long one pass of chat re-keys holds the store's write lock and the pause after it; tests hand in zeros. */
41
+ rekeyPacing?: {
42
+ lockMs: number;
43
+ pauseMs: number;
44
+ };
38
45
  /** Where raw bytes go when stdout is not this process's own — an MCP session relayed by serve. */
39
46
  stdout?: NodeJS.WritableStream;
40
47
  /** Where a prompt and what is typed at it go, in place of the process's stderr; tests hand one in. */
@@ -8,7 +8,7 @@ import { type PermissionKey } from "../../sends/permissions.js";
8
8
  import { type ServiceMessenger, type Services } from "../../services/index.js";
9
9
  import type { OpenRecognizer } from "../../speech/transcribe.js";
10
10
  import { type AccountKey, type DeletionScope, type MessageStore } from "../../store/store.js";
11
- import { type BaseContext } from "../context.js";
11
+ import { type BaseContext, type BaseEnvironment } from "../context.js";
12
12
  import { type PermissionKeyOf } from "../permission-keys.js";
13
13
  import type { GlobalFlags, ResolveOptions, Settings } from "../settings.js";
14
14
  /**
@@ -225,6 +225,8 @@ export interface MessengerContext extends BaseContext {
225
225
  name?: string;
226
226
  }) => Promise<T>;
227
227
  }
228
+ /** The waits a test shortens through its environment; a run without them keeps the defaults. */
229
+ type ConnectionWaits = Pick<BaseEnvironment, "closeWaitMs" | "rekeyPacing">;
228
230
  /**
229
231
  * A connection as every shared reader sees it: its account remembered, each call a run event, what
230
232
  * the reads answer saved to the store. `close` closes the connection and the store it opened. One
@@ -234,7 +236,7 @@ export interface MessengerContext extends BaseContext {
234
236
  * does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
235
237
  * A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
236
238
  */
237
- export declare const connected: (connection: MessengerAdapter, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }: Pick<Messenger, "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId">, { settings, env, renderer }: Pick<BaseContext, "settings" | "env" | "renderer">, events: EventSink, store?: LazyStore, { listen: watching }?: Pick<ConnectOptions, "listen">) => {
239
+ export declare const connected: (connection: MessengerAdapter, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }: Pick<Messenger, "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId">, { settings, env, renderer }: Pick<BaseContext, "settings" | "env" | "renderer">, events: EventSink, store?: LazyStore, { listen: watching, closeWaitMs, rekeyPacing, }?: Pick<ConnectOptions, "listen"> & ConnectionWaits) => {
238
240
  adapter: MessengerAdapter;
239
241
  close: () => Promise<void>;
240
242
  };
@@ -253,3 +255,4 @@ export declare const connectedFor: (command: Command, messenger: Pick<Messenger,
253
255
  */
254
256
  export declare const refuseLocalWrite: (context: MessengerContext, command: string, permission: PermissionKey) => void;
255
257
  export declare const messengerContext: (command: Command, messenger: Messenger, resolving?: Pick<ResolveOptions, "needsProfile">) => MessengerContext;
258
+ export {};
@@ -118,7 +118,7 @@ const settled = async (pending, ms) => {
118
118
  * never fails the command; one that fails once the connection is `closing` is the close, not news. It still listens
119
119
  * while the connection closes, so what arrives then is saved too.
120
120
  */
121
- const listened = ({ run, operation, what }, events, warn) => {
121
+ const listened = ({ run, operation, what }, events, warn, waitMs) => {
122
122
  const abort = new AbortController();
123
123
  let closing = false;
124
124
  const ended = Promise.resolve()
@@ -137,7 +137,7 @@ const listened = ({ run, operation, what }, events, warn) => {
137
137
  },
138
138
  stop: () => {
139
139
  abort.abort();
140
- return settled(new Set([ended]), SAVES_WAIT_MS);
140
+ return settled(new Set([ended]), waitMs);
141
141
  },
142
142
  };
143
143
  };
@@ -171,6 +171,10 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
171
171
  warn(`the recipient list still names ${from}: ${error instanceof Error ? error.message : String(error)}`);
172
172
  }
173
173
  };
174
+ const waitsOf = (command) => {
175
+ const { closeWaitMs, rekeyPacing } = environmentOf(command);
176
+ return { ...(closeWaitMs === undefined ? {} : { closeWaitMs }), ...(rekeyPacing ? { rekeyPacing } : {}) };
177
+ };
174
178
  /**
175
179
  * A connection as every shared reader sees it: its account remembered, each call a run event, what
176
180
  * the reads answer saved to the store. `close` closes the connection and the store it opened. One
@@ -180,7 +184,7 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
180
184
  * does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
181
185
  * A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
182
186
  */
183
- export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false } = {}) => {
187
+ export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false, closeWaitMs = SAVES_WAIT_MS, rekeyPacing, } = {}) => {
184
188
  const self = connection.self();
185
189
  if (self !== null)
186
190
  rememberAccount(app, settings.profile, self, env);
@@ -207,12 +211,13 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
207
211
  announcing,
208
212
  closing: closing.signal,
209
213
  ...(deletedWithoutChat ? { deletedWithoutChat } : {}),
214
+ ...(rekeyPacing ? { rekeyPacing } : {}),
210
215
  rekeyed: (from, to) => rekeyRecipients(app, settings.profile, env, renderer.warn, from, to),
211
216
  recipientIds: () => (new RecipientList(recipientsPathFor(app, settings.profile, env), app.command).read() ?? []).flatMap((recipient) => [recipient.id, ...(recipient.partnerId ? [recipient.partnerId] : [])]),
212
217
  ...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
213
218
  });
214
219
  const passives = history === "store" && self !== null && !watching
215
- ? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn))
220
+ ? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn, closeWaitMs))
216
221
  : [];
217
222
  return {
218
223
  adapter: saving,
@@ -231,7 +236,7 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
231
236
  events({ event: "warning", code: "store_not_written", operation });
232
237
  renderer.warn(`not saved to the local store: the messenger was still handing on ${what} at exit`);
233
238
  });
234
- await settled(new Set([...pending, ...announcing]), SAVES_WAIT_MS);
239
+ await settled(new Set([...pending, ...announcing]), closeWaitMs);
235
240
  closing.abort();
236
241
  if (pending.size > 0) {
237
242
  events({ event: "warning", code: "store_not_written", operation: "close" });
@@ -265,7 +270,10 @@ export const connectedFor = async (command, messenger, base, options) => {
265
270
  }
266
271
  return {
267
272
  connection,
268
- ...connected(connection, messenger, base, options.events, store, options.listen ? { listen: true } : {}),
273
+ ...connected(connection, messenger, base, options.events, store, {
274
+ ...waitsOf(command),
275
+ ...(options.listen ? { listen: true } : {}),
276
+ }),
269
277
  };
270
278
  };
271
279
  const unrecorded = (command, messenger, profile, offline) => {
@@ -410,7 +418,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
410
418
  throw new CliError("validation_error", "the read connection has already been released");
411
419
  }
412
420
  base.track(connection);
413
- held = connected(connection, messenger, base, events);
421
+ held = connected(connection, messenger, base, events, undefined, waitsOf(command));
414
422
  }
415
423
  return held.adapter;
416
424
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.359.0",
3
+ "version": "0.360.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",