@juspay/neurolink 12.54.0 → 12.54.2
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 +3 -4
- package/dist/cli/commands/proxy.js +54 -0
- package/dist/proxy/packagePrune.d.ts +8 -0
- package/dist/proxy/packagePrune.js +219 -0
- package/dist/types/proxy.d.ts +30 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
## [12.54.
|
|
1
|
+
## [12.54.2](https://github.com/juspay/neurolink/compare/v12.54.1...v12.54.2) (2026-10-12)
|
|
2
2
|
|
|
3
|
-
###
|
|
3
|
+
### Bug Fixes
|
|
4
4
|
|
|
5
|
-
- **(
|
|
6
|
-
- **(observability):** keep conversation, tool arguments and turn outcome on Vertex Claude traces ([0aa412c](https://github.com/juspay/neurolink/commit/0aa412c49634f3fd86f92179a0b45b7797877707))
|
|
5
|
+
- **(proxy):** prune superseded package builds after update and on updater start ([a9915f6](https://github.com/juspay/neurolink/commit/a9915f633825c07715cefdeb0d7587affe473f5b))
|
|
7
6
|
|
|
8
7
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
9
8
|
|
|
@@ -24,6 +24,7 @@ import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage
|
|
|
24
24
|
import { beginProxyRequest, consumeInternalProxyRequest, releaseProxyRequestAccounting, withProxyFinalLogOwnership, getProxyActivitySnapshot, observeProxyFinalLog, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
25
25
|
import { configureProxyLifecycleLogger, flushProxyLifecycleEvents, persistProxyLifecycleAcceptance, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
|
|
26
26
|
import { describeInstallFailure, assertProxyServiceInstallIdle, probeProxyLaunchdPresence, inspectProxyPackage, isProxyServiceEnvironmentKey, parseProxyServiceInstallSettings, installStagedProxyPackage, prepareProxyPackageUpgrade, readProxyPackageSelection, resolveProxyWorkerPackage, selectProxyPackage, writeProxyPackageLauncher, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
|
|
27
|
+
import { pruneProxyPackages } from "../../proxy/packagePrune.js";
|
|
27
28
|
import { startUpdaterWorkerSupervisor } from "../../proxy/updaterSupervisor.js";
|
|
28
29
|
import { openProxyWorkerLog } from "../../proxy/workerLog.js";
|
|
29
30
|
import { startRollingProxyServer } from "../../proxy/rollingProxyServer.js";
|
|
@@ -4094,13 +4095,55 @@ export const proxyGuardCommand = {
|
|
|
4094
4095
|
runtimeUpdaterPid: runtime?.updaterPid,
|
|
4095
4096
|
});
|
|
4096
4097
|
};
|
|
4098
|
+
// Each release installs a full dependency tree into its own directory and
|
|
4099
|
+
// nothing else ever removes one, so the store has to be trimmed here: on
|
|
4100
|
+
// updater start for the backlog, and after each confirmed update.
|
|
4097
4101
|
let updateInProgress = false;
|
|
4098
4102
|
let updateRestartInProgress = false;
|
|
4103
|
+
let packagePruneInFlight;
|
|
4104
|
+
const prunePackageStore = (trigger) => {
|
|
4105
|
+
packagePruneInFlight ??= (async () => {
|
|
4106
|
+
// A stale updater must not mutate package state a newer owner relies on.
|
|
4107
|
+
// An update that reuses an existing unselected build would also race the
|
|
4108
|
+
// rename; the post-update pass runs inside the update and is safe.
|
|
4109
|
+
if (!isCurrentUpdateOwner() ||
|
|
4110
|
+
(updateInProgress && trigger !== "after update")) {
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
try {
|
|
4114
|
+
const outcome = await pruneProxyPackages({
|
|
4115
|
+
packagesDir: PROXY_PACKAGES_DIR,
|
|
4116
|
+
currentEntryScript: process.argv[1],
|
|
4117
|
+
isCurrentOwner: isCurrentUpdateOwner,
|
|
4118
|
+
});
|
|
4119
|
+
if (outcome.skipped) {
|
|
4120
|
+
logger.always(`[updater] package store prune skipped (${trigger}): ${outcome.skipped}`);
|
|
4121
|
+
}
|
|
4122
|
+
else if (outcome.removed.length > 0) {
|
|
4123
|
+
logger.always(`[updater] package store prune (${trigger}): removed ${outcome.removed.length} old build(s); retained ${outcome.retained
|
|
4124
|
+
.map((build) => `${build.name} [${build.reason}]`)
|
|
4125
|
+
.join(", ")}`);
|
|
4126
|
+
}
|
|
4127
|
+
for (const failure of outcome.failed) {
|
|
4128
|
+
logger.always(`[updater] WARNING: could not remove old build ${failure.name}: ${failure.message}`);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
catch (error) {
|
|
4132
|
+
logger.always(`[updater] WARNING: package store prune failed (${trigger}): ${error instanceof Error ? error.message : String(error)}`);
|
|
4133
|
+
}
|
|
4134
|
+
})().finally(() => {
|
|
4135
|
+
packagePruneInFlight = undefined;
|
|
4136
|
+
});
|
|
4137
|
+
return packagePruneInFlight;
|
|
4138
|
+
};
|
|
4099
4139
|
const runUpdateCheck = async () => {
|
|
4100
4140
|
if (updateInProgress || !isCurrentUpdateOwner()) {
|
|
4101
4141
|
return;
|
|
4102
4142
|
}
|
|
4103
4143
|
updateInProgress = true;
|
|
4144
|
+
// A pass already under way works from a snapshot of the store and could
|
|
4145
|
+
// rename a build this update is about to reuse.
|
|
4146
|
+
await packagePruneInFlight?.catch(() => undefined);
|
|
4104
4147
|
let updateVersion = runningVersion;
|
|
4105
4148
|
let drainActive = false;
|
|
4106
4149
|
let previousPackage;
|
|
@@ -4662,6 +4705,9 @@ export const proxyGuardCommand = {
|
|
|
4662
4705
|
persistUpdaterState("record successful update", () => recordSuccessfulUpdate(result.latestVersion));
|
|
4663
4706
|
updateRetryAttempts = 0;
|
|
4664
4707
|
updateRetryVersion = null;
|
|
4708
|
+
// The new version is serving and the selection pair is committed, so
|
|
4709
|
+
// the build that just fell out of retention is unreferenced.
|
|
4710
|
+
await prunePackageStore("after update");
|
|
4665
4711
|
// The replacement proxy starts a worker running the new version.
|
|
4666
4712
|
await shutdownProxyOtelLogs().catch(() => undefined);
|
|
4667
4713
|
process.exit(0);
|
|
@@ -4769,6 +4815,14 @@ export const proxyGuardCommand = {
|
|
|
4769
4815
|
}, delay);
|
|
4770
4816
|
updateRetryTimeout.unref?.();
|
|
4771
4817
|
};
|
|
4818
|
+
// Not gated on canAutoUpdate: builds left by earlier runs are dead weight
|
|
4819
|
+
// whether or not this process will install another one. The periodic pass
|
|
4820
|
+
// is cheap (one readdir and one process listing) and covers a startup pass
|
|
4821
|
+
// that ran before the supervisor had recorded this updater as the owner.
|
|
4822
|
+
if (updaterOnly) {
|
|
4823
|
+
setTimeout(() => void prunePackageStore("startup"), 60_000).unref();
|
|
4824
|
+
setInterval(() => void prunePackageStore("periodic"), UPDATE_CHECK_INTERVAL_MS).unref();
|
|
4825
|
+
}
|
|
4772
4826
|
// Run first check after a short delay, then on interval
|
|
4773
4827
|
if (canAutoUpdate) {
|
|
4774
4828
|
updateCheckTimeout = setTimeout(runUpdateCheck, 30_000);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProxyPackagePruneOptions, ProxyPackagePruneResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Keep the selected pair (active and rollback) and remove every other build the
|
|
4
|
+
* installer generated. Nothing is removed unless the active selection is valid,
|
|
5
|
+
* the live process list could be read, and the build is neither referenced by a
|
|
6
|
+
* running process nor recently written.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pruneProxyPackages(options: ProxyPackagePruneOptions): Promise<ProxyPackagePruneResult>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { lstat, readdir, rename, rm } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { readProxyPackageSelection } from "./globalInstaller.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
// Only the names globalInstaller.ts generates: `<version>`,
|
|
10
|
+
// `<version>-release-<uuid>` and `<version>-polyfills-<digest>-<uuid>`. Anything
|
|
11
|
+
// else in the store (a hand-made `-official` copy, an in-flight `.install-*`
|
|
12
|
+
// staging tree) is not ours to delete.
|
|
13
|
+
const GENERATED_BUILD_NAME = /^\d+\.\d+\.\d+(?:-release-[0-9a-f-]{36}|-polyfills-[0-9a-f]{16}-[0-9a-f-]{36})?$/;
|
|
14
|
+
const TOMBSTONE_PREFIX = ".prune-";
|
|
15
|
+
const DEFAULT_MIN_AGE_MS = 60 * 60_000;
|
|
16
|
+
function entryScriptOf(value) {
|
|
17
|
+
return typeof value === "object" &&
|
|
18
|
+
value !== null &&
|
|
19
|
+
"entryScript" in value &&
|
|
20
|
+
typeof value.entryScript === "string"
|
|
21
|
+
? value.entryScript
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
24
|
+
function canonical(path) {
|
|
25
|
+
try {
|
|
26
|
+
return realpathSync(path);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** First path segment below the store, i.e. the build directory a file lives in. */
|
|
33
|
+
function buildDirectoryOf(storeRoot, path) {
|
|
34
|
+
const rel = relative(storeRoot, canonical(path));
|
|
35
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return rel.split(sep)[0] ?? null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read selections.json without validating the builds it names: a build that
|
|
42
|
+
* failed validation once must still count as referenced, not as garbage.
|
|
43
|
+
*/
|
|
44
|
+
function rawSelectedEntryScripts(packagesDir) {
|
|
45
|
+
try {
|
|
46
|
+
const state = JSON.parse(readFileSync(join(packagesDir, "selections.json"), "utf8"));
|
|
47
|
+
if (typeof state !== "object" || state === null) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
"active" in state ? entryScriptOf(state.active) : undefined,
|
|
52
|
+
"previous" in state ? entryScriptOf(state.previous) : undefined,
|
|
53
|
+
].filter((entry) => entry !== undefined);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function listProcessCommandLines() {
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execFileAsync("ps", ["-axo", "command="], {
|
|
65
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
66
|
+
timeout: 10_000,
|
|
67
|
+
});
|
|
68
|
+
return stdout.split("\n");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function mentionsDirectory(line, directory) {
|
|
75
|
+
return (line.includes(`${directory}${sep}`) ||
|
|
76
|
+
line.includes(`${directory} `) ||
|
|
77
|
+
line.endsWith(directory));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Keep the selected pair (active and rollback) and remove every other build the
|
|
81
|
+
* installer generated. Nothing is removed unless the active selection is valid,
|
|
82
|
+
* the live process list could be read, and the build is neither referenced by a
|
|
83
|
+
* running process nor recently written.
|
|
84
|
+
*/
|
|
85
|
+
export async function pruneProxyPackages(options) {
|
|
86
|
+
const skip = (reason) => ({
|
|
87
|
+
skipped: reason,
|
|
88
|
+
removed: [],
|
|
89
|
+
retained: [],
|
|
90
|
+
failed: [],
|
|
91
|
+
});
|
|
92
|
+
let storeRoot;
|
|
93
|
+
try {
|
|
94
|
+
storeRoot = realpathSync(options.packagesDir);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return skip("package store does not exist");
|
|
98
|
+
}
|
|
99
|
+
const buildDirectories = (entryScripts) => new Set(entryScripts
|
|
100
|
+
.filter((entry) => entry !== undefined)
|
|
101
|
+
.map((entry) => buildDirectoryOf(storeRoot, entry))
|
|
102
|
+
.filter((name) => name !== null));
|
|
103
|
+
// Read again before every removal, not once: a pass can outlive the selection
|
|
104
|
+
// it started from. Null means the active selection no longer validates.
|
|
105
|
+
const readSelected = () => {
|
|
106
|
+
const active = readProxyPackageSelection(options.packagesDir);
|
|
107
|
+
if (!active) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return buildDirectories([
|
|
111
|
+
active.entryScript,
|
|
112
|
+
readProxyPackageSelection(options.packagesDir, true)?.entryScript,
|
|
113
|
+
...rawSelectedEntryScripts(options.packagesDir),
|
|
114
|
+
]);
|
|
115
|
+
};
|
|
116
|
+
const selected = readSelected();
|
|
117
|
+
if (!selected) {
|
|
118
|
+
return skip("no validated active package selection");
|
|
119
|
+
}
|
|
120
|
+
const running = buildDirectories([options.currentEntryScript]);
|
|
121
|
+
const entries = await readdir(storeRoot, { withFileTypes: true });
|
|
122
|
+
const failed = [];
|
|
123
|
+
// A pass killed between rename and rm leaves a tombstone; nothing else reads it.
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!options.dryRun &&
|
|
126
|
+
entry.isDirectory() &&
|
|
127
|
+
entry.name.startsWith(TOMBSTONE_PREFIX)) {
|
|
128
|
+
await rm(join(storeRoot, entry.name), {
|
|
129
|
+
recursive: true,
|
|
130
|
+
force: true,
|
|
131
|
+
}).catch((error) => {
|
|
132
|
+
failed.push({ name: entry.name, message: String(error) });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const candidates = entries
|
|
137
|
+
.filter((entry) => entry.isDirectory() && GENERATED_BUILD_NAME.test(entry.name))
|
|
138
|
+
.map((entry) => entry.name)
|
|
139
|
+
.sort();
|
|
140
|
+
const unselected = candidates.filter((name) => !selected.has(name));
|
|
141
|
+
const processLines = unselected.length
|
|
142
|
+
? await (options.listProcessCommandLines ?? listProcessCommandLines)()
|
|
143
|
+
: [];
|
|
144
|
+
if (processLines === null) {
|
|
145
|
+
return skip("running processes could not be enumerated");
|
|
146
|
+
}
|
|
147
|
+
const retained = candidates
|
|
148
|
+
.filter((name) => selected.has(name))
|
|
149
|
+
.map((name) => ({ name, reason: "selected" }));
|
|
150
|
+
const removed = [];
|
|
151
|
+
const now = options.now ?? Date.now();
|
|
152
|
+
const minAgeMs = options.minAgeMs ?? DEFAULT_MIN_AGE_MS;
|
|
153
|
+
// A process may name the store through a symlinked spelling (/var vs
|
|
154
|
+
// /private/var), so match the path as configured as well as the real one.
|
|
155
|
+
const storeSpellings = [
|
|
156
|
+
...new Set([storeRoot, resolve(options.packagesDir)]),
|
|
157
|
+
];
|
|
158
|
+
for (const name of unselected) {
|
|
159
|
+
const directory = join(storeRoot, name);
|
|
160
|
+
const retain = (reason) => {
|
|
161
|
+
retained.push({ name, reason });
|
|
162
|
+
};
|
|
163
|
+
if (running.has(name) ||
|
|
164
|
+
processLines.some((line) => storeSpellings.some((store) => mentionsDirectory(line, join(store, name))))) {
|
|
165
|
+
retain("in-use");
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const stats = await lstat(directory);
|
|
170
|
+
if (now - Math.max(stats.mtimeMs, stats.ctimeMs) < minAgeMs) {
|
|
171
|
+
retain("recent");
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (options.dryRun) {
|
|
175
|
+
removed.push(name);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// The only thing left between this check and the rename is the rename. A
|
|
179
|
+
// replacement updater may have selected this build, or taken over, since
|
|
180
|
+
// the pass began.
|
|
181
|
+
const latest = readSelected();
|
|
182
|
+
if (!latest) {
|
|
183
|
+
return {
|
|
184
|
+
skipped: "package selection became invalid during the pass",
|
|
185
|
+
removed,
|
|
186
|
+
retained,
|
|
187
|
+
failed,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (latest.has(name)) {
|
|
191
|
+
retain("selected");
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (options.isCurrentOwner && !options.isCurrentOwner()) {
|
|
195
|
+
return {
|
|
196
|
+
skipped: "update ownership changed during the pass",
|
|
197
|
+
removed,
|
|
198
|
+
retained,
|
|
199
|
+
failed,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Rename first: a half-deleted directory under its real name would pass
|
|
203
|
+
// installStagedProxyPackage's existsSync() shortcut and then fail validation.
|
|
204
|
+
const tombstone = join(storeRoot, `${TOMBSTONE_PREFIX}${name}-${randomUUID()}`);
|
|
205
|
+
await rename(directory, tombstone);
|
|
206
|
+
removed.push(name);
|
|
207
|
+
await rm(tombstone, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error.code !== "ENOENT") {
|
|
211
|
+
failed.push({
|
|
212
|
+
name,
|
|
213
|
+
message: error instanceof Error ? error.message : String(error),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { removed, retained, failed };
|
|
219
|
+
}
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -4456,6 +4456,36 @@ export type ProxyPackageUpgradeOptions = ProxyStagedInstallOptions & {
|
|
|
4456
4456
|
isCurrentOwner?: () => boolean;
|
|
4457
4457
|
onPolyfills?: (report: ProxyPackagePolyfillReport) => void;
|
|
4458
4458
|
};
|
|
4459
|
+
/** Why a build in the package store was left alone by a retention pass. */
|
|
4460
|
+
export type ProxyPackagePruneRetention = "selected" | "in-use" | "recent";
|
|
4461
|
+
/** Retention pass over the immutable package store, with injectable process listing. */
|
|
4462
|
+
export type ProxyPackagePruneOptions = {
|
|
4463
|
+
packagesDir: string;
|
|
4464
|
+
/** Entry script of the calling process; its build is never removed. */
|
|
4465
|
+
currentEntryScript?: string;
|
|
4466
|
+
/** Builds written more recently than this are left for a later pass. */
|
|
4467
|
+
minAgeMs?: number;
|
|
4468
|
+
now?: number;
|
|
4469
|
+
/** Report what would be removed without touching the store. */
|
|
4470
|
+
dryRun?: boolean;
|
|
4471
|
+
/** Checked before each removal; the pass stops as soon as it returns false. */
|
|
4472
|
+
isCurrentOwner?: () => boolean;
|
|
4473
|
+
/** Command lines of live processes, or null when they cannot be listed. */
|
|
4474
|
+
listProcessCommandLines?: () => Promise<string[] | null>;
|
|
4475
|
+
};
|
|
4476
|
+
export type ProxyPackagePruneResult = {
|
|
4477
|
+
/** Set when the pass declined to remove anything or stopped early, with the reason. */
|
|
4478
|
+
skipped?: string;
|
|
4479
|
+
removed: string[];
|
|
4480
|
+
retained: {
|
|
4481
|
+
name: string;
|
|
4482
|
+
reason: ProxyPackagePruneRetention;
|
|
4483
|
+
}[];
|
|
4484
|
+
failed: {
|
|
4485
|
+
name: string;
|
|
4486
|
+
message: string;
|
|
4487
|
+
}[];
|
|
4488
|
+
};
|
|
4459
4489
|
/** Capture a trusted baseline for a locally patched package without activation. */
|
|
4460
4490
|
export type ProxyPolyfillArgs = {
|
|
4461
4491
|
action: "capture";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.54.
|
|
3
|
+
"version": "12.54.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -217,6 +217,7 @@
|
|
|
217
217
|
"test:tool-dedup": "pnpm exec tsx test/continuous-test-suite-tool-dedup.ts",
|
|
218
218
|
"test:tools-manager-truncation": "pnpm exec tsx test/continuous-test-suite-tools-manager-truncation.ts",
|
|
219
219
|
"test:tooling-scripts": "pnpm exec tsx test/continuous-test-suite-tooling-scripts.ts",
|
|
220
|
+
"test:tooling-shell-safety": "pnpm exec tsx test/continuous-test-suite-tooling-shell-safety.ts",
|
|
220
221
|
"test:model-not-found-retryable": "pnpm exec tsx test/continuous-test-suite-model-not-found-retryable.ts",
|
|
221
222
|
"test:reasoning-parity-live": "pnpm exec tsx test/continuous-test-suite-reasoning-parity-live.ts",
|
|
222
223
|
"test:ci": "pnpm run test && pnpm run test:client && pnpm run test:hitl",
|