reasonix 0.28.0 → 0.29.1

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.
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  CODE_SYSTEM_PROMPT,
4
4
  codeSystemPrompt
5
- } from "./chunk-COFBA5FV.js";
5
+ } from "./chunk-RPP24CUB.js";
6
6
  export {
7
7
  CODE_SYSTEM_PROMPT,
8
8
  codeSystemPrompt
9
9
  };
10
- //# sourceMappingURL=prompt-VF7B6BWR.js.map
10
+ //# sourceMappingURL=prompt-5GLHSLO2.js.map
package/dist/index.d.ts CHANGED
@@ -586,6 +586,15 @@ interface SessionSummary {
586
586
  }
587
587
  declare class SessionStats {
588
588
  readonly turns: TurnStats[];
589
+ /** Cost from prior runs of a resumed session, restored from session meta. */
590
+ private _carryoverCost;
591
+ /** Turn count from prior runs of a resumed session. */
592
+ private _carryoverTurns;
593
+ /** Seed totals from a resumed session's persisted meta — only call once at construction. */
594
+ seedCarryover(opts: {
595
+ totalCostUsd?: number;
596
+ turnCount?: number;
597
+ }): void;
589
598
  record(turn: number, model: string, usage: Usage): TurnStats;
590
599
  get totalCost(): number;
591
600
  get totalClaudeEquivalent(): number;
@@ -694,6 +703,8 @@ interface ToolDefinition<A = any, R = any> {
694
703
  readOnly?: boolean;
695
704
  /** Per-args check; takes precedence over `readOnly`. e.g. `run_command` + allowlisted argv. */
696
705
  readOnlyCheck?: (args: A) => boolean;
706
+ /** Safe to dispatch concurrently with other parallel-safe calls in the same turn. Default false — opt-in only. */
707
+ parallelSafe?: boolean;
697
708
  fn: (args: A, ctx?: ToolCallContext) => R | Promise<R>;
698
709
  }
699
710
  interface ToolRegistryOptions {
@@ -729,6 +740,8 @@ declare class ToolRegistry {
729
740
  get size(): number;
730
741
  /** True if a registered tool's schema was flattened for the model. */
731
742
  wasFlattened(name: string): boolean;
743
+ /** Unknown / unannotated tools default to false — third-party MCP tools must opt in. */
744
+ isParallelSafe(name: string): boolean;
732
745
  specs(): ToolSpec[];
733
746
  dispatch(name: string, argumentsRaw: string | Record<string, unknown>, opts?: {
734
747
  signal?: AbortSignal;
@@ -840,6 +853,7 @@ declare class CacheFirstLoop {
840
853
  private modelForCurrentCall;
841
854
  /** Returns true ONLY on the tipping call — caller surfaces a one-shot warning. */
842
855
  private noteToolFailureSignal;
856
+ private runOneToolCall;
843
857
  private buildMessages;
844
858
  abort(): void;
845
859
  /** Drop the last user message + everything after; caller re-sends. Persists to session file. */
@@ -1102,6 +1116,8 @@ declare function registerPlanTool(registry: ToolRegistry, opts?: PlanToolOptions
1102
1116
  /** Side-channel — subagents run inside a tool-dispatch frame, can't go through parent's `LoopEvent` stream. */
1103
1117
  interface SubagentEvent {
1104
1118
  kind: "start" | "progress" | "end" | "inner" | "phase";
1119
+ /** Stable per-spawn id; lets the UI key parallel runs apart instead of overwriting one shared row. */
1120
+ runId: string;
1105
1121
  task: string;
1106
1122
  skillName?: string;
1107
1123
  model?: string;
package/dist/index.js CHANGED
@@ -1046,6 +1046,10 @@ var ToolRegistry = class {
1046
1046
  wasFlattened(name) {
1047
1047
  return Boolean(this._tools.get(name)?.flatSchema);
1048
1048
  }
1049
+ /** Unknown / unannotated tools default to false — third-party MCP tools must opt in. */
1050
+ isParallelSafe(name) {
1051
+ return this._tools.get(name)?.parallelSafe === true;
1052
+ }
1049
1053
  specs() {
1050
1054
  return [...this._tools.values()].map((t) => ({
1051
1055
  type: "function",
@@ -1472,6 +1476,19 @@ function claudeEquivalentCost(usage) {
1472
1476
  }
1473
1477
  var SessionStats = class {
1474
1478
  turns = [];
1479
+ /** Cost from prior runs of a resumed session, restored from session meta. */
1480
+ _carryoverCost = 0;
1481
+ /** Turn count from prior runs of a resumed session. */
1482
+ _carryoverTurns = 0;
1483
+ /** Seed totals from a resumed session's persisted meta — only call once at construction. */
1484
+ seedCarryover(opts) {
1485
+ if (typeof opts.totalCostUsd === "number" && opts.totalCostUsd > 0) {
1486
+ this._carryoverCost = opts.totalCostUsd;
1487
+ }
1488
+ if (typeof opts.turnCount === "number" && opts.turnCount > 0) {
1489
+ this._carryoverTurns = opts.turnCount;
1490
+ }
1491
+ }
1475
1492
  record(turn, model, usage) {
1476
1493
  const cost = costUsd(model, usage);
1477
1494
  const stats = {
@@ -1485,7 +1502,7 @@ var SessionStats = class {
1485
1502
  return stats;
1486
1503
  }
1487
1504
  get totalCost() {
1488
- return this.turns.reduce((sum, t) => sum + t.cost, 0);
1505
+ return this._carryoverCost + this.turns.reduce((sum, t) => sum + t.cost, 0);
1489
1506
  }
1490
1507
  get totalClaudeEquivalent() {
1491
1508
  return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
@@ -1513,7 +1530,7 @@ var SessionStats = class {
1513
1530
  summary() {
1514
1531
  const last = this.turns[this.turns.length - 1];
1515
1532
  return {
1516
- turns: this.turns.length,
1533
+ turns: this.turns.length + this._carryoverTurns,
1517
1534
  totalCostUsd: round(this.totalCost, 6),
1518
1535
  totalInputCostUsd: round(this.totalInputCost, 6),
1519
1536
  totalOutputCostUsd: round(this.totalOutputCost, 6),
@@ -2520,6 +2537,13 @@ var CacheFirstLoop = class {
2520
2537
  const tokensSaved = shrunk.tokensSaved;
2521
2538
  for (const msg of messages) this.log.append(msg);
2522
2539
  this.resumedMessageCount = messages.length;
2540
+ if (messages.length > 0) {
2541
+ const meta = loadSessionMeta(this.sessionName);
2542
+ this.stats.seedCarryover({
2543
+ totalCostUsd: meta.totalCostUsd,
2544
+ turnCount: meta.turnCount
2545
+ });
2546
+ }
2523
2547
  if (healedCount > 0) {
2524
2548
  try {
2525
2549
  rewriteSession(this.sessionName, messages);
@@ -2640,6 +2664,48 @@ var CacheFirstLoop = class {
2640
2664
  this._escalateThisTurn = true;
2641
2665
  return true;
2642
2666
  }
2667
+ async runOneToolCall(call, signal) {
2668
+ const name = call.function?.name ?? "";
2669
+ const args = call.function?.arguments ?? "{}";
2670
+ const parsedArgs = safeParseToolArgs(args);
2671
+ const preReport = await runHooks({
2672
+ hooks: this.hooks,
2673
+ payload: {
2674
+ event: "PreToolUse",
2675
+ cwd: this.hookCwd,
2676
+ toolName: name,
2677
+ toolArgs: parsedArgs
2678
+ }
2679
+ });
2680
+ const preWarnings = [...hookWarnings(preReport.outcomes, this._turn)];
2681
+ if (preReport.blocked) {
2682
+ const blocking = preReport.outcomes[preReport.outcomes.length - 1];
2683
+ const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
2684
+ return {
2685
+ preWarnings,
2686
+ postWarnings: [],
2687
+ result: `[hook block] ${blocking?.hook.command ?? "<unknown>"}
2688
+ ${reason}`
2689
+ };
2690
+ }
2691
+ const result = await this.tools.dispatch(name, args, {
2692
+ signal,
2693
+ maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
2694
+ confirmationGate: this.confirmationGate
2695
+ });
2696
+ const postReport = await runHooks({
2697
+ hooks: this.hooks,
2698
+ payload: {
2699
+ event: "PostToolUse",
2700
+ cwd: this.hookCwd,
2701
+ toolName: name,
2702
+ toolArgs: parsedArgs,
2703
+ toolResult: result
2704
+ }
2705
+ });
2706
+ const postWarnings = [...hookWarnings(postReport.outcomes, this._turn)];
2707
+ return { preWarnings, postWarnings, result };
2708
+ }
2643
2709
  buildMessages(pendingUser) {
2644
2710
  const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
2645
2711
  const msgs = [...this.prefix.toMessages(), ...healed.messages];
@@ -3131,71 +3197,69 @@ var CacheFirstLoop = class {
3131
3197
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
3132
3198
  return;
3133
3199
  }
3134
- for (const call of repairedCalls) {
3135
- const name = call.function?.name ?? "";
3136
- const args = call.function?.arguments ?? "{}";
3137
- yield {
3138
- turn: this._turn,
3139
- role: "tool_start",
3140
- content: "",
3141
- toolName: name,
3142
- toolArgs: args
3143
- };
3144
- const parsedArgs = safeParseToolArgs(args);
3145
- const preReport = await runHooks({
3146
- hooks: this.hooks,
3147
- payload: {
3148
- event: "PreToolUse",
3149
- cwd: this.hookCwd,
3150
- toolName: name,
3151
- toolArgs: parsedArgs
3200
+ const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
3201
+ const parallelMaxParsed = Number.parseInt(process.env.REASONIX_PARALLEL_MAX ?? "", 10);
3202
+ const parallelMax = Number.isFinite(parallelMaxParsed) && parallelMaxParsed >= 1 ? Math.min(parallelMaxParsed, 16) : 3;
3203
+ let callIdx = 0;
3204
+ while (callIdx < repairedCalls.length) {
3205
+ const chunk = [];
3206
+ if (!dispatchSerial) {
3207
+ while (callIdx < repairedCalls.length && chunk.length < parallelMax && this.tools.isParallelSafe(repairedCalls[callIdx]?.function?.name ?? "")) {
3208
+ chunk.push(repairedCalls[callIdx++]);
3152
3209
  }
3153
- });
3154
- for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
3155
- let result;
3156
- if (preReport.blocked) {
3157
- const blocking = preReport.outcomes[preReport.outcomes.length - 1];
3158
- const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
3159
- result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
3160
- ${reason}`;
3161
- } else {
3162
- result = await this.tools.dispatch(name, args, {
3163
- signal,
3164
- maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
3165
- confirmationGate: this.confirmationGate
3166
- });
3167
- const postReport = await runHooks({
3168
- hooks: this.hooks,
3169
- payload: {
3170
- event: "PostToolUse",
3171
- cwd: this.hookCwd,
3172
- toolName: name,
3173
- toolArgs: parsedArgs,
3174
- toolResult: result
3175
- }
3176
- });
3177
- for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
3178
3210
  }
3179
- this.appendAndPersist({
3180
- role: "tool",
3181
- tool_call_id: call.id ?? "",
3182
- name,
3183
- content: result
3184
- });
3185
- if (this.noteToolFailureSignal(result)) {
3211
+ if (chunk.length === 0) {
3212
+ chunk.push(repairedCalls[callIdx++]);
3213
+ }
3214
+ for (const call of chunk) {
3186
3215
  yield {
3187
3216
  turn: this._turn,
3188
- role: "warning",
3189
- content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
3217
+ role: "tool_start",
3218
+ content: "",
3219
+ toolName: call.function?.name ?? "",
3220
+ toolArgs: call.function?.arguments ?? "{}"
3221
+ };
3222
+ }
3223
+ const settled = await Promise.allSettled(chunk.map((c) => this.runOneToolCall(c, signal)));
3224
+ for (let k = 0; k < chunk.length; k++) {
3225
+ const call = chunk[k];
3226
+ const name = call.function?.name ?? "";
3227
+ const args = call.function?.arguments ?? "{}";
3228
+ const s = settled[k];
3229
+ let result;
3230
+ let preWarnings = [];
3231
+ let postWarnings = [];
3232
+ if (s.status === "fulfilled") {
3233
+ preWarnings = s.value.preWarnings;
3234
+ postWarnings = s.value.postWarnings;
3235
+ result = s.value.result;
3236
+ } else {
3237
+ const err = s.reason instanceof Error ? s.reason : new Error(String(s.reason));
3238
+ result = JSON.stringify({ error: `${err.name}: ${err.message}` });
3239
+ }
3240
+ for (const w of preWarnings) yield w;
3241
+ for (const w of postWarnings) yield w;
3242
+ this.appendAndPersist({
3243
+ role: "tool",
3244
+ tool_call_id: call.id ?? "",
3245
+ name,
3246
+ content: result
3247
+ });
3248
+ if (this.noteToolFailureSignal(result)) {
3249
+ yield {
3250
+ turn: this._turn,
3251
+ role: "warning",
3252
+ content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
3253
+ };
3254
+ }
3255
+ yield {
3256
+ turn: this._turn,
3257
+ role: "tool",
3258
+ content: result,
3259
+ toolName: name,
3260
+ toolArgs: args
3190
3261
  };
3191
3262
  }
3192
- yield {
3193
- turn: this._turn,
3194
- role: "tool",
3195
- content: result,
3196
- toolName: name,
3197
- toolArgs: args
3198
- };
3199
3263
  }
3200
3264
  }
3201
3265
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
@@ -4604,6 +4668,7 @@ function registerFilesystemTools(registry, opts) {
4604
4668
  };
4605
4669
  registry.register({
4606
4670
  name: "read_file",
4671
+ parallelSafe: true,
4607
4672
  description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
4608
4673
  - head: N \u2192 first N lines (imports, public API, small configs)
4609
4674
  - tail: N \u2192 last N lines (recently-added code, log tails)
@@ -4687,6 +4752,7 @@ ${slice.join("\n")}`;
4687
4752
  });
4688
4753
  registry.register({
4689
4754
  name: "list_directory",
4755
+ parallelSafe: true,
4690
4756
  description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
4691
4757
  readOnly: true,
4692
4758
  parameters: {
@@ -4707,6 +4773,7 @@ ${slice.join("\n")}`;
4707
4773
  });
4708
4774
  registry.register({
4709
4775
  name: "directory_tree",
4776
+ parallelSafe: true,
4710
4777
  description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
4711
4778
  - maxDepth defaults to 2 (root + one level). A depth-4 tree on a real repo blew ~5K tokens in one call. If you truly need deeper, pass maxDepth:N explicitly.
4712
4779
  - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
@@ -4785,6 +4852,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4785
4852
  });
4786
4853
  registry.register({
4787
4854
  name: "search_files",
4855
+ parallelSafe: true,
4788
4856
  description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) by default.",
4789
4857
  readOnly: true,
4790
4858
  parameters: {
@@ -4810,6 +4878,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4810
4878
  });
4811
4879
  registry.register({
4812
4880
  name: "search_content",
4881
+ parallelSafe: true,
4813
4882
  description: "Recursively grep file CONTENTS for a substring or regex. This is the right tool for 'find all places that call X', 'where is Y referenced', 'what files contain Z'. Different from search_files (which matches FILE NAMES). Returns one match per line in 'path:line: text' format. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) and binary files by default.",
4814
4883
  readOnly: true,
4815
4884
  parameters: {
@@ -4852,6 +4921,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
4852
4921
  });
4853
4922
  registry.register({
4854
4923
  name: "get_file_info",
4924
+ parallelSafe: true,
4855
4925
  description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
4856
4926
  readOnly: true,
4857
4927
  parameters: {
@@ -5032,6 +5102,7 @@ function registerMemoryTools(registry, opts = {}) {
5032
5102
  name: "recall_memory",
5033
5103
  description: "Read the full body of a memory file when its MEMORY.md one-liner (already in the system prompt) isn't enough detail. Most of the time the index suffices \u2014 only call this when the user's question genuinely requires the full context.",
5034
5104
  readOnly: true,
5105
+ parallelSafe: true,
5035
5106
  parameters: {
5036
5107
  type: "object",
5037
5108
  properties: {
@@ -5458,6 +5529,11 @@ function getSubagentType(name) {
5458
5529
  }
5459
5530
 
5460
5531
  // src/tools/subagent.ts
5532
+ var runIdCounter = 0;
5533
+ function nextRunId() {
5534
+ runIdCounter++;
5535
+ return `sub-${runIdCounter.toString(36)}`;
5536
+ }
5461
5537
  var DEFAULT_SUBAGENT_SYSTEM = `You are a Reasonix subagent. The parent agent spawned you to handle one focused subtask, then return.
5462
5538
 
5463
5539
  Rules:
@@ -5486,9 +5562,11 @@ async function spawnSubagent(opts) {
5486
5562
  const sink = opts.sink;
5487
5563
  const skillName = opts.skillName;
5488
5564
  const startedAt = Date.now();
5565
+ const runId = nextRunId();
5489
5566
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
5490
5567
  sink?.current?.({
5491
5568
  kind: "start",
5569
+ runId,
5492
5570
  task: taskPreview,
5493
5571
  skillName,
5494
5572
  model,
@@ -5501,6 +5579,7 @@ async function spawnSubagent(opts) {
5501
5579
  const errorMessage2 = `subagent allow-list names tool(s) not registered in the parent: ${missing.join(", ")}. Fix the skill's \`allowed-tools\` frontmatter or check spelling.`;
5502
5580
  sink?.current?.({
5503
5581
  kind: "end",
5582
+ runId,
5504
5583
  task: taskPreview,
5505
5584
  skillName,
5506
5585
  model,
@@ -5562,12 +5641,13 @@ async function spawnSubagent(opts) {
5562
5641
  let summarisingEmitted = false;
5563
5642
  try {
5564
5643
  for await (const ev of childLoop.step(opts.task)) {
5565
- sink?.current?.({ kind: "inner", task: taskPreview, skillName, model, inner: ev });
5644
+ sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model, inner: ev });
5566
5645
  if (ev.role === "tool") {
5567
5646
  toolIter++;
5568
5647
  summarisingEmitted = false;
5569
5648
  sink?.current?.({
5570
5649
  kind: "progress",
5650
+ runId,
5571
5651
  task: taskPreview,
5572
5652
  skillName,
5573
5653
  model,
@@ -5579,6 +5659,7 @@ async function spawnSubagent(opts) {
5579
5659
  summarisingEmitted = true;
5580
5660
  sink?.current?.({
5581
5661
  kind: "phase",
5662
+ runId,
5582
5663
  task: taskPreview,
5583
5664
  skillName,
5584
5665
  model,
@@ -5615,6 +5696,7 @@ async function spawnSubagent(opts) {
5615
5696
  [\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
5616
5697
  sink?.current?.({
5617
5698
  kind: "end",
5699
+ runId,
5618
5700
  task: taskPreview,
5619
5701
  skillName,
5620
5702
  model,
@@ -5678,6 +5760,7 @@ function registerSubagentTool(parentRegistry, opts) {
5678
5760
  const sink = opts.sink;
5679
5761
  parentRegistry.register({
5680
5762
  name: SUBAGENT_TOOL_NAME,
5763
+ parallelSafe: true,
5681
5764
  description: "Spawn an isolated subagent to handle a self-contained subtask in a fresh context, returning only its final answer. Use for: deep codebase exploration that would flood the main context, multi-step research where you only need the conclusion, or any focused subtask whose intermediate reasoning the user does not need to see. The subagent inherits all your tools (filesystem, shell, web, MCP, etc.) but runs in its own isolated message log \u2014 its tool calls and reasoning never enter your context. Only the final assistant message comes back as this tool's result. Keep tasks focused; the subagent has a stricter iter budget than you do.",
5682
5765
  parameters: {
5683
5766
  type: "object",
@@ -7103,6 +7186,7 @@ function registerShellTools(registry, opts) {
7103
7186
  name: "job_output",
7104
7187
  description: "Read the latest output of a background job started with `run_background`. By default returns the tail of the buffer (last 80 lines). Pass `since` (the `byteLength` from a previous call) to stream only new content incrementally. Tells you whether the job is still running, so you can stop polling when it's done.",
7105
7188
  readOnly: true,
7189
+ parallelSafe: true,
7106
7190
  parameters: {
7107
7191
  type: "object",
7108
7192
  properties: {
@@ -7147,6 +7231,7 @@ function registerShellTools(registry, opts) {
7147
7231
  name: "list_jobs",
7148
7232
  description: "List every background job started this session \u2014 running and exited \u2014 with id, command, pid, status. Use when you've lost track of which job_id corresponds to which process, or to see what's still alive.",
7149
7233
  readOnly: true,
7234
+ parallelSafe: true,
7150
7235
  parameters: { type: "object", properties: {} },
7151
7236
  fn: async () => {
7152
7237
  const all = jobs.list();
@@ -7404,6 +7489,7 @@ function registerWebTools(registry, opts = {}) {
7404
7489
  name: "web_search",
7405
7490
  description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this.",
7406
7491
  readOnly: true,
7492
+ parallelSafe: true,
7407
7493
  parameters: {
7408
7494
  type: "object",
7409
7495
  properties: {
@@ -7427,6 +7513,7 @@ function registerWebTools(registry, opts = {}) {
7427
7513
  name: "web_fetch",
7428
7514
  description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
7429
7515
  readOnly: true,
7516
+ parallelSafe: true,
7430
7517
  parameters: {
7431
7518
  type: "object",
7432
7519
  properties: {