@wirecat/cli-core 0.359.0 → 0.361.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 +26 -0
- package/dist/cli/context.d.ts +7 -0
- package/dist/cli/messenger/context.d.ts +5 -2
- package/dist/cli/messenger/context.js +28 -19
- package/dist/cli/messenger/serve-notice.d.ts +3 -2
- package/dist/cli/messenger/serve-notice.js +6 -4
- package/dist/cli/messenger/stored.d.ts +2 -2
- package/dist/cli/messenger/stored.js +14 -6
- package/dist/runs/events.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.361.0 — 19.10.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A warning in `serve`'s log now says which warning it was.** Off a terminal, a re-key given up at exit, a write
|
|
15
|
+
the store did not take, a passive listen that failed and a stored-chats call that failed each wrote a bare
|
|
16
|
+
`serve.warning` record, so the log could not tell them apart. The record now carries the warning's `code`,
|
|
17
|
+
`operation` and `counts` (`rekeys_not_applied` with `counts.rekeys`), never its sentence, which can quote an
|
|
18
|
+
error or a chat. Warnings that have no code yet still log a bare record.
|
|
19
|
+
- **The adapter test example in [ADAPTERS.md](docs/dev/ADAPTERS.md#running-the-contract-cases) no longer gives up
|
|
20
|
+
re-keys.** It handed `run()` `rekeyPacing: { lockMs: 0 }`, one re-key per transaction, so a few hundred re-keys
|
|
21
|
+
outlasted `closeWaitMs: 10` and were given up. It now hands `{ lockMs: 1000, pauseMs: 0 }` and says what each
|
|
22
|
+
field does.
|
|
23
|
+
|
|
24
|
+
## 0.360.0 — 19.10.2026
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Adapter test authors can shorten a connection's close wait and re-key pauses.** The run environment takes
|
|
29
|
+
`closeWaitMs` (how long a connection's close waits on its saves and passive listens, 5 s by default) and
|
|
30
|
+
`rekeyPacing` (`{ lockMs, pauseMs }` for chat re-keys, 50 ms and 25 ms by default). Commands, the shared services and
|
|
31
|
+
the MCP server's tool calls pass them on; a command `serve` runs for a client through its relay keeps the
|
|
32
|
+
defaults. A test through `run()` no longer pays the real waits. Only code
|
|
33
|
+
that builds the environment can set them; no flag, variable or MCP call does
|
|
34
|
+
([ADAPTERS.md](docs/dev/ADAPTERS.md#running-the-contract-cases)).
|
|
35
|
+
|
|
10
36
|
## 0.359.0 — 19.10.2026
|
|
11
37
|
|
|
12
38
|
### Fixed
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -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 no pause. */
|
|
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()
|
|
@@ -126,8 +126,9 @@ const listened = ({ run, operation, what }, events, warn) => {
|
|
|
126
126
|
.catch((error) => {
|
|
127
127
|
if (closing)
|
|
128
128
|
return;
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const warning = { event: "warning", code: "listen_failed", operation };
|
|
130
|
+
events(warning);
|
|
131
|
+
warn(`${what} arriving during this command are not saved: ${error instanceof Error ? error.message : String(error)}`, warning);
|
|
131
132
|
});
|
|
132
133
|
return {
|
|
133
134
|
operation,
|
|
@@ -137,7 +138,7 @@ const listened = ({ run, operation, what }, events, warn) => {
|
|
|
137
138
|
},
|
|
138
139
|
stop: () => {
|
|
139
140
|
abort.abort();
|
|
140
|
-
return settled(new Set([ended]),
|
|
141
|
+
return settled(new Set([ended]), waitMs);
|
|
141
142
|
},
|
|
142
143
|
};
|
|
143
144
|
};
|
|
@@ -171,6 +172,10 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
|
|
|
171
172
|
warn(`the recipient list still names ${from}: ${error instanceof Error ? error.message : String(error)}`);
|
|
172
173
|
}
|
|
173
174
|
};
|
|
175
|
+
const waitsOf = (command) => {
|
|
176
|
+
const { closeWaitMs, rekeyPacing } = environmentOf(command);
|
|
177
|
+
return { ...(closeWaitMs === undefined ? {} : { closeWaitMs }), ...(rekeyPacing ? { rekeyPacing } : {}) };
|
|
178
|
+
};
|
|
174
179
|
/**
|
|
175
180
|
* A connection as every shared reader sees it: its account remembered, each call a run event, what
|
|
176
181
|
* the reads answer saved to the store. `close` closes the connection and the store it opened. One
|
|
@@ -180,7 +185,7 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
|
|
|
180
185
|
* does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
|
|
181
186
|
* A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
|
|
182
187
|
*/
|
|
183
|
-
export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false } = {}) => {
|
|
188
|
+
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
189
|
const self = connection.self();
|
|
185
190
|
if (self !== null)
|
|
186
191
|
rememberAccount(app, settings.profile, self, env);
|
|
@@ -207,12 +212,13 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
207
212
|
announcing,
|
|
208
213
|
closing: closing.signal,
|
|
209
214
|
...(deletedWithoutChat ? { deletedWithoutChat } : {}),
|
|
215
|
+
...(rekeyPacing ? { rekeyPacing } : {}),
|
|
210
216
|
rekeyed: (from, to) => rekeyRecipients(app, settings.profile, env, renderer.warn, from, to),
|
|
211
217
|
recipientIds: () => (new RecipientList(recipientsPathFor(app, settings.profile, env), app.command).read() ?? []).flatMap((recipient) => [recipient.id, ...(recipient.partnerId ? [recipient.partnerId] : [])]),
|
|
212
218
|
...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
|
|
213
219
|
});
|
|
214
220
|
const passives = history === "store" && self !== null && !watching
|
|
215
|
-
? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn))
|
|
221
|
+
? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn, closeWaitMs))
|
|
216
222
|
: [];
|
|
217
223
|
return {
|
|
218
224
|
adapter: saving,
|
|
@@ -225,22 +231,22 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
225
231
|
finally {
|
|
226
232
|
try {
|
|
227
233
|
const ended = await Promise.all(passives.map((passive) => passive.stop()));
|
|
234
|
+
const warn = renderer.warn;
|
|
235
|
+
const warned = (warning, text) => {
|
|
236
|
+
events(warning);
|
|
237
|
+
warn(text, warning);
|
|
238
|
+
};
|
|
228
239
|
passives.forEach(({ operation, what }, at) => {
|
|
229
240
|
if (ended[at])
|
|
230
241
|
return;
|
|
231
|
-
|
|
232
|
-
renderer.warn(`not saved to the local store: the messenger was still handing on ${what} at exit`);
|
|
242
|
+
warned({ event: "warning", code: "store_not_written", operation }, `not saved to the local store: the messenger was still handing on ${what} at exit`);
|
|
233
243
|
});
|
|
234
|
-
await settled(new Set([...pending, ...announcing]),
|
|
244
|
+
await settled(new Set([...pending, ...announcing]), closeWaitMs);
|
|
235
245
|
closing.abort();
|
|
236
|
-
if (pending.size > 0)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (announcing.size > 0) {
|
|
241
|
-
events({ event: "warning", code: "stored_chats_failed", operation: "close" });
|
|
242
|
-
renderer.warn("the messenger's stored-chats hook did not finish and was abandoned at exit; nothing is lost");
|
|
243
|
-
}
|
|
246
|
+
if (pending.size > 0)
|
|
247
|
+
warned({ event: "warning", code: "store_not_written", operation: "close" }, "not saved to the local store: the last messages were still being written at exit");
|
|
248
|
+
if (announcing.size > 0)
|
|
249
|
+
warned({ event: "warning", code: "stored_chats_failed", operation: "close" }, "the messenger's stored-chats hook did not finish and was abandoned at exit; nothing is lost");
|
|
244
250
|
}
|
|
245
251
|
finally {
|
|
246
252
|
await store.close();
|
|
@@ -265,7 +271,10 @@ export const connectedFor = async (command, messenger, base, options) => {
|
|
|
265
271
|
}
|
|
266
272
|
return {
|
|
267
273
|
connection,
|
|
268
|
-
...connected(connection, messenger, base, options.events, store,
|
|
274
|
+
...connected(connection, messenger, base, options.events, store, {
|
|
275
|
+
...waitsOf(command),
|
|
276
|
+
...(options.listen ? { listen: true } : {}),
|
|
277
|
+
}),
|
|
269
278
|
};
|
|
270
279
|
};
|
|
271
280
|
const unrecorded = (command, messenger, profile, offline) => {
|
|
@@ -410,7 +419,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
|
|
|
410
419
|
throw new CliError("validation_error", "the read connection has already been released");
|
|
411
420
|
}
|
|
412
421
|
base.track(connection);
|
|
413
|
-
held = connected(connection, messenger, base, events);
|
|
422
|
+
held = connected(connection, messenger, base, events, undefined, waitsOf(command));
|
|
414
423
|
}
|
|
415
424
|
return held.adapter;
|
|
416
425
|
},
|
|
@@ -13,8 +13,9 @@ export declare const noticeFor: (context: MessengerContext, app: AppIdentity, se
|
|
|
13
13
|
/**
|
|
14
14
|
* A serve left running writes its diagnostics to a log file, so off a terminal each one is a log record with
|
|
15
15
|
* its time and run. A sentence handed to the renderer may quote an error or a chat, so the record keeps only
|
|
16
|
-
* which kind of line it was
|
|
17
|
-
*
|
|
16
|
+
* which kind of line it was, and a warning's code, operation and counts when the warning event comes with it
|
|
17
|
+
* (`Warn`); serve's own reports go through `noticeFor` with a code. The renderer is changed in place, so the
|
|
18
|
+
* send guard, bound to it before serve started, writes records too.
|
|
18
19
|
*/
|
|
19
20
|
export declare const recordDiagnostics: (context: MessengerContext, app: AppIdentity) => MessengerContext;
|
|
20
21
|
export {};
|
|
@@ -19,16 +19,18 @@ export const noticeFor = (context, app, severity = "warn") => ({ text, ...attrib
|
|
|
19
19
|
/**
|
|
20
20
|
* A serve left running writes its diagnostics to a log file, so off a terminal each one is a log record with
|
|
21
21
|
* its time and run. A sentence handed to the renderer may quote an error or a chat, so the record keeps only
|
|
22
|
-
* which kind of line it was
|
|
23
|
-
*
|
|
22
|
+
* which kind of line it was, and a warning's code, operation and counts when the warning event comes with it
|
|
23
|
+
* (`Warn`); serve's own reports go through `noticeFor` with a code. The renderer is changed in place, so the
|
|
24
|
+
* send guard, bound to it before serve started, writes records too.
|
|
24
25
|
*/
|
|
25
26
|
export const recordDiagnostics = (context, app) => {
|
|
26
27
|
if (context.format === "pretty")
|
|
27
28
|
return context;
|
|
28
|
-
const say = (severity, event, always = false) => (_text) => {
|
|
29
|
+
const say = (severity, event, always = false) => (_text, warning) => {
|
|
29
30
|
if (context.settings.quiet && !always)
|
|
30
31
|
return;
|
|
31
|
-
|
|
32
|
+
const { event: _warning, ...fields } = warning ?? {};
|
|
33
|
+
record(context, app, severity, { event, ...fields });
|
|
32
34
|
};
|
|
33
35
|
const renderer = context.renderer;
|
|
34
36
|
renderer.note = say("info", "serve.note");
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Id } from "../../domain/models.js";
|
|
2
2
|
import { type MessengerAdapter } from "../../ports/messenger.js";
|
|
3
3
|
import type { MessengerTraits } from "../../ports/traits.js";
|
|
4
|
-
import type { EventSink } from "../../runs/events.js";
|
|
4
|
+
import type { EventSink, Warn } from "../../runs/events.js";
|
|
5
5
|
import type { AccountKey, DeletionScope, MessageStore } from "../../store/store.js";
|
|
6
6
|
export interface Saving {
|
|
7
7
|
account: AccountKey;
|
|
8
8
|
/** Opened on the first save, not before: a command that reads nothing has no reason to touch the file. */
|
|
9
9
|
store: () => Promise<MessageStore | undefined>;
|
|
10
|
-
warn:
|
|
10
|
+
warn: Warn;
|
|
11
11
|
events: EventSink;
|
|
12
12
|
/** Saves still writing, for the closer to wait on: `watch` does not hold the next event for one. */
|
|
13
13
|
pending?: Set<Promise<void>>;
|
|
@@ -16,9 +16,10 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
16
16
|
let warned = false;
|
|
17
17
|
let recorded = profile === undefined;
|
|
18
18
|
const notWritten = (operation, error) => {
|
|
19
|
-
|
|
19
|
+
const warning = { event: "warning", code: "store_not_written", operation };
|
|
20
|
+
events(warning);
|
|
20
21
|
if (!warned)
|
|
21
|
-
warn(`not saved to the local store: ${error instanceof Error ? error.message : String(error)}
|
|
22
|
+
warn(`not saved to the local store: ${error instanceof Error ? error.message : String(error)}`, warning);
|
|
22
23
|
warned = true;
|
|
23
24
|
};
|
|
24
25
|
const attempt = async (operation, write) => {
|
|
@@ -92,8 +93,14 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
92
93
|
pending?.delete(saving);
|
|
93
94
|
if (count === 0)
|
|
94
95
|
return;
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const warning = {
|
|
97
|
+
event: "warning",
|
|
98
|
+
code: "rekeys_not_applied",
|
|
99
|
+
operation: "history.rekey",
|
|
100
|
+
counts: { rekeys: count },
|
|
101
|
+
};
|
|
102
|
+
events(warning);
|
|
103
|
+
warn(`${count} chat re-key${count === 1 ? "" : "s"} not applied yet; the next serve applies them`, warning);
|
|
97
104
|
};
|
|
98
105
|
closing?.addEventListener("abort", giveUpRekeys, { once: true });
|
|
99
106
|
const saveRekeys = async (pairs, round) => {
|
|
@@ -466,8 +473,9 @@ const announceStoredChats = async (messenger, store, account, events, warn, reci
|
|
|
466
473
|
await messenger.storedChats?.(held);
|
|
467
474
|
}
|
|
468
475
|
catch (error) {
|
|
469
|
-
|
|
470
|
-
|
|
476
|
+
const warning = { event: "warning", code: "stored_chats_failed", operation: "history.stored_chats" };
|
|
477
|
+
events(warning);
|
|
478
|
+
warn(`the messenger was not told the stored chats: ${error instanceof Error ? error.message : String(error)}`, warning);
|
|
471
479
|
}
|
|
472
480
|
};
|
|
473
481
|
/** Read once when `watch` starts: a store read per event would let a later event overtake an earlier one. */
|
package/dist/runs/events.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface CacheEvent {
|
|
|
49
49
|
}
|
|
50
50
|
export type DiagnosticEvent = RequestEvent | CacheEvent | WarningEvent;
|
|
51
51
|
export type EventSink = (event: DiagnosticEvent) => void;
|
|
52
|
+
/** A sentence for a person, and the warning event it stands for when there is one: a log record keeps only the event. */
|
|
53
|
+
export type Warn = (message: string, warning?: WarningEvent) => void;
|
|
52
54
|
/** `FLOOD_WAIT`, `PEER_ID_INVALID`, `login.token` — or nothing. A sentence is dropped, not trimmed. */
|
|
53
55
|
export declare const providerErrorKey: (value: unknown) => string | undefined;
|
|
54
56
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.361.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",
|