chainlesschain 0.166.133 → 0.166.135
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/README.md +11 -3
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/data/changelog.json +13 -0
- package/src/lib/background-launch-profile.js +37 -1
- package/src/lib/coding-task-guidance.js +10 -9
- package/src/lib/context-memory-kernel/message-adapter.js +5 -1
- package/src/runtime/agent-core.js +117 -28
- package/src/runtime/headless-runner.js +3 -3
package/README.md
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Command-line interface for installing, configuring, and managing [ChainlessChain](https://www.chainlesschain.com), including the coding agent, governed tools, sessions, memory, and IDE integrations.
|
|
4
4
|
|
|
5
|
-
## CLI 0.166.
|
|
5
|
+
## CLI 0.166.135
|
|
6
6
|
|
|
7
|
-
This
|
|
7
|
+
This release recovers genuine OpenAI Responses JSON disconnects before accepted output, bounds stalled requests, and settles completed streaming responses without waiting for socket EOF. It preserves empty tool allowlists across resume and rejects malformed saved permission, sandbox and budget settings. HTTP/provider failures, cancellation, usage persistence and incomplete tool-call replay protections remain authoritative.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Install with `npm i -g chainlesschain@0.166.135 --registry https://registry.npmjs.org`, restart the chat host, then run `cc --version` and `cc doctor`. Paired IDE versions are Open VSX **0.37.172** and JetBrains **0.4.189**, recommending CLI **0.166.135**; Kernel stays **0.1.11**. Check the [release guide](https://docs.chainlesschain.com/chainlesschain/agent-platform-release.html) for channel publication status. Microsoft Marketplace and PC/mobile releases are outside this release scope.
|
|
10
|
+
|
|
11
|
+
The preceding OpenAI native cohort accepted **39/45** tasks. Runtime regression tests and this release do not change that historical result or establish formal paid/native production admission.
|
|
12
|
+
|
|
13
|
+
## CLI 0.166.133 — verified preceding release
|
|
14
|
+
|
|
15
|
+
CLI **0.166.133** is published on npm. It preserves required empty OpenAI Responses reasoning summaries, applies the self-evolution switch to all model-owning `cc ui` entry points, and supports explicitly configured OpenAI-compatible local/LAN platforms through `custom`, with optional credentials and unchanged endpoint/model aliases. It also exposes canonical allowed mutation paths in provider tool schemas while retaining execution-time scope and file-identity checks. Controlled regressions and release gates do not establish live-platform task acceptance.
|
|
16
|
+
|
|
17
|
+
Install with `npm i -g chainlesschain@0.166.133 --registry https://registry.npmjs.org`, restart the chat host, then run `cc --version` and `cc doctor`. Companion Open VSX **0.37.171** / JetBrains **0.4.188** are public and recommend this CLI; their original payloads match the official artifacts. Kernel stays **0.1.11**. Exact source `2eb0c5b1e048509362f9d980f94acee55c2ffa9f` passed Linux, Windows and macOS CLI CI / CLI Strict Sandbox. [OIDC publication](https://github.com/chainlesschain/chainlesschain/actions/runs/41073612529) and [independent readback](https://github.com/chainlesschain/chainlesschain/actions/runs/41075409835) verified immutable package bytes and signed provenance. Consult the [current release guide](https://docs.chainlesschain.com/chainlesschain/agent-platform-release.html) for IDE channel availability. Microsoft Marketplace and PC/mobile releases are outside this release scope.
|
|
10
18
|
|
|
11
19
|
## CLI 0.166.132 — verified public baseline before this source revision
|
|
12
20
|
|
package/package.json
CHANGED
package/src/data/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "CHANGELOG.md",
|
|
3
3
|
"releases": [
|
|
4
|
+
{
|
|
5
|
+
"productVersion": null,
|
|
6
|
+
"cliVersion": "0.166.135",
|
|
7
|
+
"cliVersions": ["0.166.135"],
|
|
8
|
+
"date": "2026-10-31",
|
|
9
|
+
"title": "",
|
|
10
|
+
"sections": [
|
|
11
|
+
{
|
|
12
|
+
"heading": "Fixed - cc CLI 0.166.135: Responses disconnect recovery and launch policy preservation (candidate, 2026-10-31)",
|
|
13
|
+
"body": "- Recover genuine pre-output OpenAI Responses JSON disconnects with the existing bounded retry policy, distinct physical request IDs and synchronous usage settlement. Bound stalled headers and JSON reads, preserve HTTP/provider verdicts and caller cancellation, and keep workflow effects and incomplete tool streams non-replayable.\n- Settle completed Responses events without waiting for socket EOF. A hung reader cancellation no longer blocks timeout settlement; already queued transport aborts and user cancellation remain authoritative.\n- Select newly generated context handoffs before ordinary conversation so a full conversation partition cannot evict its summary. Retain the same input budget, data-only trust and oversized-summary rejection.\n- Preserve an explicit empty tool allowlist through saved launch argv and the CLI parser. Reject malformed persisted permission modes, sandbox modes and budgets instead of silently restoring weaker defaults.\n- Strengthen generic regression guidance for delegated lifecycle ownership, race controls, persisted history projections and a final response within the iteration budget. Add same-handle metadata-race and approval-cancellation controls without changing historical model candidates.\n- Give independent IDE cleanup a separate bounded deadline after the model task ends. The previous `verify-26` trace actually contains `error_max_turns` at 20/20; cleanup had obscured that result with an expired-deadline error. Historical OpenAI acceptance remains 39/45 and is not reclassified by these fixes.\n- Prepare companion Open VSX 0.37.172 and JetBrains 0.4.189 for CLI 0.166.135. Required child npm packages retain their published versions; publication must follow the exact-source multi-platform gates and upstream readback."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"productVersion": null,
|
|
6
19
|
"cliVersion": "0.166.133",
|
|
@@ -666,6 +666,41 @@ export function normalizeBackgroundLaunchProfile(input) {
|
|
|
666
666
|
`Unsupported background launch profile version: ${input?.version ?? "missing"}`,
|
|
667
667
|
);
|
|
668
668
|
}
|
|
669
|
+
const rejectField = (field) => {
|
|
670
|
+
const error = new Error(
|
|
671
|
+
`Invalid background launch profile field: ${field}`,
|
|
672
|
+
);
|
|
673
|
+
error.code = "BACKGROUND_LAUNCH_PROFILE_INVALID";
|
|
674
|
+
throw error;
|
|
675
|
+
};
|
|
676
|
+
// A persisted policy must not silently fall back to a weaker default.
|
|
677
|
+
if (
|
|
678
|
+
input.permission?.mode != null &&
|
|
679
|
+
!PERMISSION_MODES.has(input.permission.mode)
|
|
680
|
+
)
|
|
681
|
+
rejectField("permission.mode");
|
|
682
|
+
if (input.sandbox?.mode != null && !SANDBOX_MODES.has(input.sandbox.mode))
|
|
683
|
+
rejectField("sandbox.mode");
|
|
684
|
+
for (const field of [
|
|
685
|
+
"maxTurns",
|
|
686
|
+
"maxCostUsd",
|
|
687
|
+
"thinkingTokens",
|
|
688
|
+
"maxRewakes",
|
|
689
|
+
"maxOuterTurns",
|
|
690
|
+
"goalMaxTokens",
|
|
691
|
+
"goalMaxCostUsd",
|
|
692
|
+
"goalMaxTimeMs",
|
|
693
|
+
]) {
|
|
694
|
+
const value = input.budget?.[field];
|
|
695
|
+
if (value == null || value === "") continue;
|
|
696
|
+
const number = Number(value);
|
|
697
|
+
if (
|
|
698
|
+
!Number.isFinite(number) ||
|
|
699
|
+
number <= 0 ||
|
|
700
|
+
(!field.toLowerCase().includes("cost") && !Number.isSafeInteger(number))
|
|
701
|
+
)
|
|
702
|
+
rejectField(`budget.${field}`);
|
|
703
|
+
}
|
|
669
704
|
const cwd = canonicalPath(input.workspace?.cwd || process.cwd());
|
|
670
705
|
const safeBase = sanitizeBackgroundBaseUrl(input.llm?.baseUrl);
|
|
671
706
|
const permissionMode = PERMISSION_MODES.has(input.permission?.mode)
|
|
@@ -966,7 +1001,8 @@ export function buildArgvFromBackgroundLaunchProfile(profile) {
|
|
|
966
1001
|
pushValue(argv, "--fallback-model", model);
|
|
967
1002
|
}
|
|
968
1003
|
if (p.tools.allowed !== null) {
|
|
969
|
-
|
|
1004
|
+
// Empty means no tools; omitting this option would allow every tool.
|
|
1005
|
+
argv.push("--allowed-tools", p.tools.allowed.join(","));
|
|
970
1006
|
}
|
|
971
1007
|
pushValue(argv, "--disallowed-tools", p.tools.disallowed.join(","));
|
|
972
1008
|
pushValue(argv, "--permission-mode", p.permission.mode);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/** Shared correctness guidance for normal and file-only coding sessions. */
|
|
2
2
|
export const CODING_TASK_QUALITY_GUIDANCE = `Coding task quality:
|
|
3
|
-
- Read the actual implementation and its exported API before writing a caller or test.
|
|
4
|
-
- Keep progress explanations to one or two sentences.
|
|
5
|
-
- Resolve each relative import from the destination file's directory. Read back
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
- For
|
|
9
|
-
- Trace rejection before asserting its aftermath:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
3
|
+
- Read the actual implementation and its exported API before writing a caller or test. Verify parameter order, return shapes, fixture requirements and the delegated owner of each lifecycle transition.
|
|
4
|
+
- Keep progress explanations to one or two sentences. Resolve uncertainty with targeted reads, reuse unchanged source already inspected, and put code directly in write/edit arguments.
|
|
5
|
+
- Resolve each relative import from the destination file's directory. Read back completed files and fix newDiagnostics. A successful write proves persistence, not test execution or acceptance.
|
|
6
|
+
- Map every acceptance requirement to a fixture, real production call path and observable assertion. Positive and negative controls must reach the same boundary with unrelated prerequisites valid. Avoid mocks that bypass it; identify which assertion would fail without that boundary.
|
|
7
|
+
- Derive case allocation from the user's requirements. A test-count cap does not waive coverage: merge redundant cases to cover gaps within the cap, authorized scope and remaining budget. Report unresolved gaps as incomplete.
|
|
8
|
+
- For races, control the intended transition and complete the current worker/request when testing cancellation or replacement. An earlier unrelated rejection does not prove the target guard. Derive expectations independently from the contract or authoritative state, not by calling the function under test.
|
|
9
|
+
- Trace rejection before asserting its aftermath: it may precede registration or persistence. Inspect whether state stays unchanged, a failed record exists or an event is emitted. Compare independently recorded identities and provenance; equal text does not prove ownership.
|
|
10
|
+
- Inspect persistence projections before comparing histories. Host notices, lineage and imports may accompany user/assistant messages; verify their provenance separately. Report defects that cannot be repaired within the permitted scope instead of inventing passing behavior.
|
|
11
|
+
- Run focused checks with permitted execution tools. With file tools only, inspect imports, contracts and fixtures and say tests were not run. Read-back or static inspection is not a passing test.
|
|
12
|
+
- Finish requested edits and read-back before the final reply; reserve the last available model request for a concise terminal account. For large files, write a small complete section, await its result, then extend it with a unique edit anchor rather than rewriting the growing file. Report delivery, actual checks and limitations once.`;
|
|
12
13
|
|
|
13
14
|
export const HERMETIC_CODING_SYSTEM_PROMPT = `You are a hermetic coding assistant. The exposed file tools are the complete tool surface. Inspect and mutate only the exact paths authorized by the user request. Do not invoke hidden tools, commands, language servers or other execution paths.
|
|
14
15
|
|
|
@@ -549,7 +549,11 @@ export function createSummaryContextItem({
|
|
|
549
549
|
Math.ceil((Buffer.byteLength(content, "utf8") - image.bytes) / 4) +
|
|
550
550
|
image.tokens,
|
|
551
551
|
),
|
|
552
|
-
|
|
552
|
+
// Keep the new handoff ahead of ordinary conversation in the final plan.
|
|
553
|
+
// Otherwise newer messages can fill the partition and evict the summary
|
|
554
|
+
// of the already-dropped history. This is selection priority only: the
|
|
555
|
+
// summary stays unpinned, data-only and subject to the same token ceilings.
|
|
556
|
+
priority: Math.max(600_000, ...parents.map((item) => item.priority)),
|
|
553
557
|
pinned: false,
|
|
554
558
|
createdAt: new Date(now).toISOString(),
|
|
555
559
|
...contentFields,
|
|
@@ -12671,34 +12671,69 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
12671
12671
|
);
|
|
12672
12672
|
}
|
|
12673
12673
|
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12674
|
+
// A JSON-body disconnect has produced no accepted output or executable
|
|
12675
|
+
// tools. Apply the same bounded transport retry and usage settlement used
|
|
12676
|
+
// by streaming; HTTP/provider verdicts and governed workflow calls retain
|
|
12677
|
+
// their existing no-replay rules.
|
|
12678
|
+
return await _retryStreamingChat(
|
|
12679
|
+
(attempt = 0) =>
|
|
12680
|
+
_withResponsesRequestDeadline(
|
|
12681
|
+
async (requestSignal) => {
|
|
12682
|
+
const requestId = supportsOpenAIRequestIdentity
|
|
12683
|
+
? _physicalProviderRequestId(providerRequestId, attempt)
|
|
12684
|
+
: null;
|
|
12685
|
+
const response = await providerFetch(`${url}/responses`, {
|
|
12686
|
+
method: "POST",
|
|
12687
|
+
headers: {
|
|
12688
|
+
"Content-Type": "application/json",
|
|
12689
|
+
Authorization: `Bearer ${key}`,
|
|
12690
|
+
...(requestId ? { "X-Client-Request-Id": requestId } : {}),
|
|
12691
|
+
},
|
|
12692
|
+
signal: requestSignal,
|
|
12693
|
+
body: JSON.stringify(responseBody),
|
|
12694
|
+
});
|
|
12695
|
+
if (!response.ok) {
|
|
12696
|
+
throw await createProviderResponseError(provider, response);
|
|
12697
|
+
}
|
|
12698
|
+
const retryMetadata = captureStreamRetryMetadata(response);
|
|
12699
|
+
try {
|
|
12700
|
+
const data = await response.json();
|
|
12701
|
+
const out = normalizeOpenAIResponsesResponse(data);
|
|
12702
|
+
if (out.usage)
|
|
12703
|
+
out.usage = preserveRequestedServiceTierEvidence(
|
|
12704
|
+
out.usage,
|
|
12705
|
+
options.serviceTier,
|
|
12706
|
+
);
|
|
12707
|
+
delete out.responseId;
|
|
12708
|
+
const providerReceipt = supportsOpenAIRequestIdentity
|
|
12709
|
+
? _openAIProviderRequestReceipt(response, data, requestId)
|
|
12710
|
+
: null;
|
|
12711
|
+
if (providerReceipt) out.providerReceipt = providerReceipt;
|
|
12712
|
+
return out;
|
|
12713
|
+
} catch (error) {
|
|
12714
|
+
attachStreamRetryMetadata(error, retryMetadata);
|
|
12715
|
+
throw error;
|
|
12716
|
+
}
|
|
12717
|
+
},
|
|
12718
|
+
signal,
|
|
12719
|
+
options.streamStallTimeoutMs,
|
|
12720
|
+
),
|
|
12721
|
+
{
|
|
12722
|
+
signal,
|
|
12723
|
+
retries: options.workflowEffectId ? 0 : undefined,
|
|
12724
|
+
strictRetryObserver: options.strictUsageTelemetry === true,
|
|
12725
|
+
...(typeof options.onStreamRetry === "function"
|
|
12726
|
+
? {
|
|
12727
|
+
onRetry: (attempt, error, telemetry) =>
|
|
12728
|
+
options.onStreamRetry(attempt, error, {
|
|
12729
|
+
...telemetry,
|
|
12730
|
+
provider,
|
|
12731
|
+
model: effectiveModel,
|
|
12732
|
+
}),
|
|
12733
|
+
}
|
|
12681
12734
|
: {}),
|
|
12682
12735
|
},
|
|
12683
|
-
|
|
12684
|
-
body: JSON.stringify(responseBody),
|
|
12685
|
-
});
|
|
12686
|
-
if (!response.ok) {
|
|
12687
|
-
throw await createProviderResponseError(provider, response);
|
|
12688
|
-
}
|
|
12689
|
-
const data = await response.json();
|
|
12690
|
-
const out = normalizeOpenAIResponsesResponse(data);
|
|
12691
|
-
if (out.usage)
|
|
12692
|
-
out.usage = preserveRequestedServiceTierEvidence(
|
|
12693
|
-
out.usage,
|
|
12694
|
-
options.serviceTier,
|
|
12695
|
-
);
|
|
12696
|
-
delete out.responseId;
|
|
12697
|
-
const providerReceipt = supportsOpenAIRequestIdentity
|
|
12698
|
-
? _openAIProviderRequestReceipt(response, data, providerRequestId)
|
|
12699
|
-
: null;
|
|
12700
|
-
if (providerReceipt) out.providerReceipt = providerReceipt;
|
|
12701
|
-
return out;
|
|
12736
|
+
);
|
|
12702
12737
|
}
|
|
12703
12738
|
|
|
12704
12739
|
// Real token streaming (--include-partial-messages) for every OpenAI-compatible
|
|
@@ -13047,6 +13082,36 @@ const STREAM_STALL_MS = 20000;
|
|
|
13047
13082
|
const STREAM_STALL_TIMEOUT_MS = 180000;
|
|
13048
13083
|
const _STREAM_STALL = Symbol("stream-stall");
|
|
13049
13084
|
|
|
13085
|
+
/** Bound the whole JSON attempt, including headers and a stalled body read. */
|
|
13086
|
+
async function _withResponsesRequestDeadline(
|
|
13087
|
+
operation,
|
|
13088
|
+
signal,
|
|
13089
|
+
configuredTimeout,
|
|
13090
|
+
) {
|
|
13091
|
+
const timeoutMs = configuredTimeout ?? STREAM_STALL_TIMEOUT_MS;
|
|
13092
|
+
if (timeoutMs === 0) return operation(signal);
|
|
13093
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 2_147_483_647)
|
|
13094
|
+
throw new RangeError("Invalid Responses request timeout");
|
|
13095
|
+
const controller = new AbortController();
|
|
13096
|
+
const requestSignal = signal
|
|
13097
|
+
? AbortSignal.any([signal, controller.signal])
|
|
13098
|
+
: controller.signal;
|
|
13099
|
+
const timer = setTimeout(() => {
|
|
13100
|
+
const error = new Error(
|
|
13101
|
+
"OpenAI Responses request timed out before its complete response was received",
|
|
13102
|
+
);
|
|
13103
|
+
error.code = "ETIMEDOUT";
|
|
13104
|
+
controller.abort(error);
|
|
13105
|
+
}, timeoutMs);
|
|
13106
|
+
timer.unref?.();
|
|
13107
|
+
try {
|
|
13108
|
+
throwIfAborted(requestSignal);
|
|
13109
|
+
return await raceWithAbort(operation(requestSignal), requestSignal);
|
|
13110
|
+
} finally {
|
|
13111
|
+
clearTimeout(timer);
|
|
13112
|
+
}
|
|
13113
|
+
}
|
|
13114
|
+
|
|
13050
13115
|
/**
|
|
13051
13116
|
* Run a streaming chat attempt with bounded auto-retry on transient API
|
|
13052
13117
|
* connection drops (Claude-Code 2.1.181: "auto-retry for API connection drops
|
|
@@ -13176,7 +13241,9 @@ export async function* _iterateStreamWithStall(reader, opts = {}) {
|
|
|
13176
13241
|
// socket) and throw a retryable error the dispatch seam re-issues.
|
|
13177
13242
|
if (timeoutMs && elapsed >= timeoutMs) {
|
|
13178
13243
|
try {
|
|
13179
|
-
|
|
13244
|
+
// Cancellation itself can hang on a broken transport. Observe its
|
|
13245
|
+
// rejection, but never let it postpone the authoritative timeout.
|
|
13246
|
+
void Promise.resolve(reader.cancel()).catch(() => {});
|
|
13180
13247
|
} catch {
|
|
13181
13248
|
/* best-effort release — the throw below is what matters */
|
|
13182
13249
|
}
|
|
@@ -13474,7 +13541,7 @@ async function _chatOpenAIResponsesStreaming(
|
|
|
13474
13541
|
const decoder = new TextDecoder();
|
|
13475
13542
|
let buf = "";
|
|
13476
13543
|
try {
|
|
13477
|
-
for await (const value of _iterateStreamWithStall(reader, {
|
|
13544
|
+
responses: for await (const value of _iterateStreamWithStall(reader, {
|
|
13478
13545
|
onStall,
|
|
13479
13546
|
stallMs,
|
|
13480
13547
|
stallTimeoutMs,
|
|
@@ -13485,6 +13552,22 @@ async function _chatOpenAIResponsesStreaming(
|
|
|
13485
13552
|
for (const line of lines) {
|
|
13486
13553
|
reduceOpenAIResponsesStreamLine(state, line, ui.onToken, ui.onThinking);
|
|
13487
13554
|
await ui.settle();
|
|
13555
|
+
if (state.terminalError) throw state.terminalError;
|
|
13556
|
+
if (state.normalized) {
|
|
13557
|
+
// response.completed/incomplete is authoritative. Some gateways
|
|
13558
|
+
// leave the HTTP stream alive after it; socket EOF is not required.
|
|
13559
|
+
// Observe an already-queued transport error (especially cancellation)
|
|
13560
|
+
// without waiting for a pending trailing read to finish.
|
|
13561
|
+
await Promise.race([reader.read(), Promise.resolve()]);
|
|
13562
|
+
throwIfAborted(signal);
|
|
13563
|
+
try {
|
|
13564
|
+
void Promise.resolve(reader.cancel()).catch(() => {});
|
|
13565
|
+
} catch {
|
|
13566
|
+
// The terminal response is already complete; cleanup is best effort.
|
|
13567
|
+
}
|
|
13568
|
+
buf = "";
|
|
13569
|
+
break responses;
|
|
13570
|
+
}
|
|
13488
13571
|
}
|
|
13489
13572
|
}
|
|
13490
13573
|
if (buf.trim()) {
|
|
@@ -13526,6 +13609,12 @@ async function _chatOpenAIResponsesStreaming(
|
|
|
13526
13609
|
);
|
|
13527
13610
|
if (providerReceipt) out.providerReceipt = providerReceipt;
|
|
13528
13611
|
return out;
|
|
13612
|
+
} finally {
|
|
13613
|
+
try {
|
|
13614
|
+
reader.releaseLock?.();
|
|
13615
|
+
} catch {
|
|
13616
|
+
// A cancelled or still-settling reader can reject lock release.
|
|
13617
|
+
}
|
|
13529
13618
|
}
|
|
13530
13619
|
const out = finalizeOpenAIResponsesStream(state);
|
|
13531
13620
|
if (out.usage)
|
|
@@ -467,15 +467,15 @@ export function resolveEnabledTools({ allowedTools, readOnly } = {}) {
|
|
|
467
467
|
return names;
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
/**
|
|
470
|
+
/** An absent CLI list is unrestricted; an explicit empty list permits none. */
|
|
471
471
|
export function parseToolList(value) {
|
|
472
|
-
if (
|
|
472
|
+
if (value === null || value === undefined) return null;
|
|
473
473
|
if (Array.isArray(value)) return value.flatMap((v) => parseToolList(v) || []);
|
|
474
474
|
const out = String(value)
|
|
475
475
|
.split(/[,\s]+/)
|
|
476
476
|
.map((s) => s.trim())
|
|
477
477
|
.filter(Boolean);
|
|
478
|
-
return out
|
|
478
|
+
return out;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
/**
|