@wirecat/cli-core 0.354.0 → 0.355.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,21 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.355.0 — 19.10.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A failed scheduled run no longer keeps a chat title or a name in `last.json`** (privacy). Its `reason` was the
|
|
15
|
+
last line the run printed, which is the whole error object, message included: "no chat matches …" and the list of
|
|
16
|
+
chats or people an ambiguous name matches reached the file and the "scheduled … failing" notice. `last.json` now
|
|
17
|
+
keeps the error's `code` as the new `errorCode` and a `reason` of the code and its reason id, such as
|
|
18
|
+
`validation_error (input.invalid)`; a last line that is not an error object, such as an agent harness's, is kept
|
|
19
|
+
only as `exit <n>`. The full message stays on stderr and in the run's log, whose path `schedule list` now prints
|
|
20
|
+
after a failing schedule's reason. `last.json` is written atomically with mode 0600 even over an older file.
|
|
21
|
+
`reasonOf(stderr, exitCode?)` follows and always answers a string, and `recordRun` takes the optional `errorCode`.
|
|
22
|
+
A background `store` sync job's file likewise keeps only the error's code and names `store jobs show <id>`, which
|
|
23
|
+
prints the path of the job's log.
|
|
24
|
+
|
|
10
25
|
## 0.354.0 — 19.10.2026
|
|
11
26
|
|
|
12
27
|
### Fixed
|
|
@@ -25,15 +25,26 @@ export interface LastRun {
|
|
|
25
25
|
output: string;
|
|
26
26
|
/** Shared by every command the run started (`TRACEPARENT`), so `runs list` names them. */
|
|
27
27
|
traceId?: string;
|
|
28
|
-
/** Why a failed run failed, in one line: the
|
|
28
|
+
/** Why a failed run failed, in one line: the error's code and reason id, never its message. */
|
|
29
29
|
reason?: string;
|
|
30
|
+
/** The `code` of the error the run printed, as run records keep it. */
|
|
31
|
+
errorCode?: string;
|
|
30
32
|
/** The first failure of the failures in a row, so "failing since" survives every retry. */
|
|
31
33
|
failingSince?: string;
|
|
32
34
|
/** Failed runs in a row; absent once a run succeeds. */
|
|
33
35
|
failures?: number;
|
|
34
36
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Why a failed run failed, from the error object the CLI printed last: its code and reason id. Never the
|
|
39
|
+
* message, which can name a chat or a person; the run's log keeps that. Any other line is another program's
|
|
40
|
+
* text and is not kept either.
|
|
41
|
+
*/
|
|
42
|
+
export declare const failureOf: (stderr: string, exitCode: number) => {
|
|
43
|
+
reason: string;
|
|
44
|
+
errorCode?: string;
|
|
45
|
+
};
|
|
46
|
+
/** The one-line reason `failureOf` keeps, for a timer that reads a run's stderr itself. */
|
|
47
|
+
export declare const reasonOf: (stderr: string, exitCode?: number) => string;
|
|
37
48
|
/**
|
|
38
49
|
* Writes one run's result to `path`, carrying the failure streak over from the run before. Any CLI's own
|
|
39
50
|
* timer can call this, so `failingNotice` reads every tool's background runs the same way.
|
|
@@ -44,6 +55,7 @@ export declare const recordRun: (path: string, run: {
|
|
|
44
55
|
output: string;
|
|
45
56
|
traceId?: string;
|
|
46
57
|
reason?: string;
|
|
58
|
+
errorCode?: string;
|
|
47
59
|
}) => LastRun;
|
|
48
60
|
/**
|
|
49
61
|
* One stderr line per failing background job, or nothing: `tg: scheduled digest failing since … (3 runs):
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { CliError, writeSecurely } from "@wirecat/cli-base";
|
|
4
|
+
import { CliError, errorCodes, writeSecurely } from "@wirecat/cli-base";
|
|
5
5
|
import { readOnlyLockOf } from "../ports/app.js";
|
|
6
6
|
import { recordLogFor } from "../runs/record-log.js";
|
|
7
7
|
import { childTrace } from "../runs/scope.js";
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const REASON_ID = /^[a-z0-9][a-z0-9.-]{0,79}$/;
|
|
9
|
+
/**
|
|
10
|
+
* Why a failed run failed, from the error object the CLI printed last: its code and reason id. Never the
|
|
11
|
+
* message, which can name a chat or a person; the run's log keeps that. Any other line is another program's
|
|
12
|
+
* text and is not kept either.
|
|
13
|
+
*/
|
|
14
|
+
export const failureOf = (stderr, exitCode) => {
|
|
15
|
+
for (const line of stderr.split("\n").reverse()) {
|
|
16
|
+
try {
|
|
17
|
+
const { code, reason } = JSON.parse(line).error ?? {};
|
|
18
|
+
if (errorCodes.includes(code) && typeof reason === "string" && REASON_ID.test(reason)) {
|
|
19
|
+
return { reason: `${code} (${reason})`, errorCode: code };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
}
|
|
24
|
+
return { reason: `exit ${exitCode}` };
|
|
17
25
|
};
|
|
26
|
+
/** The one-line reason `failureOf` keeps, for a timer that reads a run's stderr itself. */
|
|
27
|
+
export const reasonOf = (stderr, exitCode = 1) => failureOf(stderr, exitCode).reason;
|
|
18
28
|
const readRun = (path) => {
|
|
19
29
|
try {
|
|
20
30
|
return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : undefined;
|
|
@@ -29,17 +39,17 @@ const readRun = (path) => {
|
|
|
29
39
|
*/
|
|
30
40
|
export const recordRun = (path, run) => {
|
|
31
41
|
const before = readRun(path);
|
|
32
|
-
const { reason, ...rest } = run;
|
|
42
|
+
const { reason, errorCode, ...rest } = run;
|
|
33
43
|
const last = run.exitCode === 0
|
|
34
44
|
? rest
|
|
35
45
|
: {
|
|
36
46
|
...rest,
|
|
37
47
|
...(reason === undefined ? {} : { reason }),
|
|
48
|
+
...(errorCode === undefined ? {} : { errorCode }),
|
|
38
49
|
failingSince: before?.exitCode ? (before.failingSince ?? before.at) : run.at,
|
|
39
50
|
failures: before?.exitCode ? (before.failures ?? 1) + 1 : 1,
|
|
40
51
|
};
|
|
41
|
-
|
|
42
|
-
writeFileSync(path, `${JSON.stringify(last, null, 2)}\n`, { mode: 0o600 });
|
|
52
|
+
writeSecurely(path, `${JSON.stringify(last, null, 2)}\n`, 0o600);
|
|
43
53
|
return last;
|
|
44
54
|
};
|
|
45
55
|
const shortTime = (iso) => {
|
|
@@ -371,13 +381,12 @@ export const runSchedule = async (app, system, env, file, { now = () => new Date
|
|
|
371
381
|
exitCode,
|
|
372
382
|
durationMs: ended.getTime() - began,
|
|
373
383
|
});
|
|
374
|
-
const said = exitCode === 0 ? undefined : reasonOf(readFileSync(errors, "utf8"));
|
|
375
384
|
const last = recordRun(join(folder, "last.json"), {
|
|
376
385
|
at: ended.toISOString(),
|
|
377
386
|
exitCode,
|
|
378
387
|
output,
|
|
379
388
|
traceId: trace.traceId,
|
|
380
|
-
...(
|
|
389
|
+
...(exitCode === 0 ? {} : failureOf(readFileSync(errors, "utf8"), exitCode)),
|
|
381
390
|
});
|
|
382
391
|
if (agent && exitCode === 0)
|
|
383
392
|
copyFileSync(output, join(folder, "latest.md"));
|
|
@@ -175,7 +175,8 @@ export const syncCommand = (messenger) => {
|
|
|
175
175
|
catch (error) {
|
|
176
176
|
if (jobId) {
|
|
177
177
|
const code = isCliFailure(error) ? error.code : "generic_failure";
|
|
178
|
-
|
|
178
|
+
// The job file outlives the run; the message can name a chat, so only the job's log keeps it.
|
|
179
|
+
const message = `${code} — \`store jobs show ${jobId}\` names the log with the whole error`;
|
|
179
180
|
finishJob(messenger.app, jobs, jobId, { finishedAt: new Date().toISOString(), error: { code, message } });
|
|
180
181
|
}
|
|
181
182
|
throw error;
|
|
@@ -185,7 +185,7 @@ export const scheduleCommand = (messenger) => {
|
|
|
185
185
|
? `last ${last.at.slice(0, 16).replace("T", " ")} exit ${last.exitCode}` +
|
|
186
186
|
(last.failures
|
|
187
187
|
? ` — failing since ${(last.failingSince ?? last.at).slice(0, 16).replace("T", " ")}, ` +
|
|
188
|
-
`${last.failures} in a row${last.reason ? `: ${last.reason}` : ""}`
|
|
188
|
+
`${last.failures} in a row${last.reason ? `: ${last.reason}` : ""} — ${last.output}`
|
|
189
189
|
: "")
|
|
190
190
|
: "not run yet",
|
|
191
191
|
].join(" "))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.355.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",
|