@scalequality/cli 0.4.8 → 0.4.9-rc.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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "2cc2b586649f475725d306b032e821c01721bcd6bfa560f8423ad9de3e189050",
4
- "sourceCommit": "b252300744f0d3132910cba3d700faf23a2c468b"
3
+ "sha256": "5ad74aa136be8c43759fe4a89f977345c3af7ba6fea4c85657bab56398b3da4f",
4
+ "sourceCommit": "38ed6bf736d29da672865dfedeed01901e57b828"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -2602,6 +2602,28 @@ var ConsoleLog = class {
2602
2602
  }
2603
2603
  }
2604
2604
  };
2605
+ function turnTimingLine(ctx) {
2606
+ if (!ctx || ctx.event !== "WORKSPACE_TURN_TIMING") return null;
2607
+ const ms = (k) => typeof ctx[k] === "number" ? ctx[k] : null;
2608
+ const secs = (v) => v >= 1e4 ? `${(v / 1e3).toFixed(1)} s` : `${Math.round(v)} ms`;
2609
+ const parts = [];
2610
+ const first = ms("firstModelEventMs");
2611
+ parts.push(`first model event ${first === null ? "none" : secs(first)}`);
2612
+ const phases = [
2613
+ ms("prepareMs") !== null ? `prepare ${secs(ms("prepareMs"))}` : null,
2614
+ ctx.warmStart === true ? "engine started ahead" : ctx.warmStart === false ? "engine started for it" : null,
2615
+ ms("engineReadyMs") !== null ? `engine ready ${secs(ms("engineReadyMs"))}` : null,
2616
+ ms("checkpointMs") !== null ? `saved point ${secs(ms("checkpointMs"))}` : null,
2617
+ ms("toolWaitMs") !== null ? `tools waited ${secs(ms("toolWaitMs"))}` : null
2618
+ ].filter(Boolean);
2619
+ if (phases.length) parts.push(`(${phases.join(", ")})`);
2620
+ const permissions = ms("permissions");
2621
+ if (permissions) parts.push(`; ${permissions} permission${permissions === 1 ? "" : "s"} ${secs(ms("permissionMs") ?? 0)}`);
2622
+ const total = ms("totalMs");
2623
+ if (total !== null) parts.push(`; total ${secs(total)}`);
2624
+ if (typeof ctx.outcome === "string" && ctx.outcome !== "completed") parts.push(`(${ctx.outcome})`);
2625
+ return `Timing: ${parts.join(" ").replace(/ ;/g, ";")}`;
2626
+ }
2605
2627
  function banner(boot, local, style2, warnings) {
2606
2628
  const model = boot.runtime.primaryModel || boot.model;
2607
2629
  const repos = bootScopeRepos(boot);
@@ -2965,6 +2987,7 @@ function removeStableCopies(home, keep = null) {
2965
2987
  function serviceArgs(env, bin, api) {
2966
2988
  return [env.nodePath, bin, "up", "--api", api, "--service"];
2967
2989
  }
2990
+ var LAUNCHD_PROCESS_TYPE = "Interactive";
2968
2991
  var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2969
2992
  function launchAgentPlist(env, bin, api) {
2970
2993
  const names = serviceNames(api);
@@ -2990,7 +3013,7 @@ ${args}
2990
3013
  <key>ThrottleInterval</key>
2991
3014
  <integer>10</integer>
2992
3015
  <key>ProcessType</key>
2993
- <string>Background</string>
3016
+ <string>${LAUNCHD_PROCESS_TYPE}</string>
2994
3017
  <key>WorkingDirectory</key>
2995
3018
  <string>${xml(env.home)}</string>
2996
3019
  <key>EnvironmentVariables</key>
@@ -4411,7 +4434,7 @@ function limitInEngineText(text3) {
4411
4434
  // src/application/services/workspaceSandbox/scalequalityTools.ts
4412
4435
  var import_crypto4 = require("crypto");
4413
4436
 
4414
- // ../aigov-code-queue/node_modules/zod/v3/external.js
4437
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/external.js
4415
4438
  var external_exports = {};
4416
4439
  __export(external_exports, {
4417
4440
  BRAND: () => BRAND,
@@ -4523,7 +4546,7 @@ __export(external_exports, {
4523
4546
  void: () => voidType
4524
4547
  });
4525
4548
 
4526
- // ../aigov-code-queue/node_modules/zod/v3/helpers/util.js
4549
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/util.js
4527
4550
  var util;
4528
4551
  (function(util2) {
4529
4552
  util2.assertEqual = (_) => {
@@ -4657,7 +4680,7 @@ var getParsedType = (data) => {
4657
4680
  }
4658
4681
  };
4659
4682
 
4660
- // ../aigov-code-queue/node_modules/zod/v3/ZodError.js
4683
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/ZodError.js
4661
4684
  var ZodIssueCode = util.arrayToEnum([
4662
4685
  "invalid_type",
4663
4686
  "invalid_literal",
@@ -4775,7 +4798,7 @@ ZodError.create = (issues) => {
4775
4798
  return error;
4776
4799
  };
4777
4800
 
4778
- // ../aigov-code-queue/node_modules/zod/v3/locales/en.js
4801
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/locales/en.js
4779
4802
  var errorMap = (issue, _ctx) => {
4780
4803
  let message;
4781
4804
  switch (issue.code) {
@@ -4878,7 +4901,7 @@ var errorMap = (issue, _ctx) => {
4878
4901
  };
4879
4902
  var en_default = errorMap;
4880
4903
 
4881
- // ../aigov-code-queue/node_modules/zod/v3/errors.js
4904
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/errors.js
4882
4905
  var overrideErrorMap = en_default;
4883
4906
  function setErrorMap(map) {
4884
4907
  overrideErrorMap = map;
@@ -4887,7 +4910,7 @@ function getErrorMap() {
4887
4910
  return overrideErrorMap;
4888
4911
  }
4889
4912
 
4890
- // ../aigov-code-queue/node_modules/zod/v3/helpers/parseUtil.js
4913
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/parseUtil.js
4891
4914
  var makeIssue = (params) => {
4892
4915
  const { data, path, errorMaps, issueData } = params;
4893
4916
  const fullPath = [...path, ...issueData.path || []];
@@ -4997,14 +5020,14 @@ var isDirty = (x) => x.status === "dirty";
4997
5020
  var isValid = (x) => x.status === "valid";
4998
5021
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
4999
5022
 
5000
- // ../aigov-code-queue/node_modules/zod/v3/helpers/errorUtil.js
5023
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/errorUtil.js
5001
5024
  var errorUtil;
5002
5025
  (function(errorUtil2) {
5003
5026
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
5004
5027
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
5005
5028
  })(errorUtil || (errorUtil = {}));
5006
5029
 
5007
- // ../aigov-code-queue/node_modules/zod/v3/types.js
5030
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/types.js
5008
5031
  var ParseInputLazyPath = class {
5009
5032
  constructor(parent, value, path, key) {
5010
5033
  this._cachedPath = [];
@@ -9164,6 +9187,28 @@ var TurnTiming = class {
9164
9187
  calls = 0;
9165
9188
  answers = /* @__PURE__ */ new Set();
9166
9189
  done = false;
9190
+ firstFrame = null;
9191
+ phase = {};
9192
+ permissionTotal = 0;
9193
+ permissionCount = 0;
9194
+ toolWaitTotal = 0;
9195
+ /** Durations (ms) and flags of the turn's own phases. */
9196
+ phases(p) {
9197
+ Object.assign(this.phase, p);
9198
+ }
9199
+ /** Any frame from the engine process (the first one says it is up and has the message). */
9200
+ engineFrame() {
9201
+ if (this.firstFrame === null) this.firstFrame = this.now();
9202
+ }
9203
+ /** A command or change permission the engine asked for (the rules, the person's mode or the person answered). */
9204
+ permission(ms) {
9205
+ this.permissionTotal += ms;
9206
+ this.permissionCount += 1;
9207
+ }
9208
+ /** A tool waited for the turn's saved point. */
9209
+ toolWait(ms) {
9210
+ this.toolWaitTotal += ms;
9211
+ }
9167
9212
  /** Any frame of the main conversation that came from the model. */
9168
9213
  modelEvent() {
9169
9214
  if (this.firstModel === null) this.firstModel = this.now();
@@ -9200,6 +9245,10 @@ var TurnTiming = class {
9200
9245
  firstVisibleEventMs: since(this.firstVisible),
9201
9246
  totalMs: end - this.startedAt,
9202
9247
  modelCalls: this.calls,
9248
+ ...this.phase,
9249
+ engineReadyMs: since(this.firstFrame),
9250
+ ...this.permissionCount ? { permissions: this.permissionCount, permissionMs: this.permissionTotal } : {},
9251
+ ...this.toolWaitTotal ? { toolWaitMs: this.toolWaitTotal } : {},
9203
9252
  outcome,
9204
9253
  ...extra
9205
9254
  });
@@ -9836,6 +9885,7 @@ ${lines2.join("\n")}
9836
9885
  }
9837
9886
 
9838
9887
  // src/application/services/workspaceSandbox/WorkspaceEngine.ts
9888
+ var WARM_IDLE_MS = 15 * 6e4;
9839
9889
  var STOP_GRACE_MS = 8e3;
9840
9890
  var INTERRUPT_TIMEOUT_MS = 3e3;
9841
9891
  var MODEL_TOOLS = ["Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep", "Bash", "TaskStop", "TodoWrite", "Agent", "WebFetch", "Skill"];
@@ -10032,6 +10082,14 @@ var WorkspaceEngine = class {
10032
10082
  envTrouble = [];
10033
10083
  /** The organization's approved MCP servers: their catalog and the in-process servers that forward to the API. */
10034
10084
  orgMcp = null;
10085
+ /**
10086
+ * The saved point of the turn the engine runs now: every tool waits for it (the PreToolUse hook and the permission
10087
+ * prompt), so the engine process starts while it is taken and no tool changes a file before it is recorded.
10088
+ */
10089
+ toolGate = Promise.resolve();
10090
+ /** The engine process started for the next turn (warmStart), and the start options it was given (their hash). */
10091
+ warm = null;
10092
+ prewarming = null;
10035
10093
  emit(e) {
10036
10094
  if (e.type === "terminal" && e.data.chunk === void 0 && this.folderBaseline) this.sweepPending = true;
10037
10095
  if (this.turnWatch) {
@@ -10221,6 +10279,7 @@ var WorkspaceEngine = class {
10221
10279
  if (this.resumedFromCheckpoint) await this.diffNow();
10222
10280
  this.phase("READY");
10223
10281
  await this.sink.flush();
10282
+ this.prewarm();
10224
10283
  return true;
10225
10284
  }
10226
10285
  /** Where a background command writes its output: inside the config dir, which Read may read. */
@@ -10556,6 +10615,7 @@ var WorkspaceEngine = class {
10556
10615
  } finally {
10557
10616
  this.turnRunning = null;
10558
10617
  }
10618
+ this.prewarm();
10559
10619
  })();
10560
10620
  }
10561
10621
  /** Waits for the current turn (test and shutdown helper). */
@@ -10839,14 +10899,16 @@ ${String(item.content)}` : String(item.content);
10839
10899
  this.recordCheckpoint(turnId, await this.takeSnapshot());
10840
10900
  }
10841
10901
  async takeSnapshot() {
10902
+ const sdkSessionId = this.sdkSessionId;
10903
+ const resumeAt = this.resumeAt ?? this.lastChainUuid;
10842
10904
  const trees = /* @__PURE__ */ new Map();
10843
- await this.folderBaseline;
10905
+ await this.folderBaseline?.catch(() => void 0);
10844
10906
  for (const repo2 of this.repos.values()) {
10845
10907
  if (repo2.dormant) continue;
10846
10908
  const tree = await repo2.tracker.snapshot().catch(() => null);
10847
10909
  if (tree) trees.set(repo2.root, tree);
10848
10910
  }
10849
- return { trees, sdkSessionId: this.sdkSessionId, resumeAt: this.resumeAt ?? this.lastChainUuid };
10911
+ return { trees, sdkSessionId, resumeAt };
10850
10912
  }
10851
10913
  recordCheckpoint(turnId, snap) {
10852
10914
  const at = iso2();
@@ -11220,9 +11282,16 @@ ${json}
11220
11282
  this.turnWatch = { lastExit: null, errored: false };
11221
11283
  if (!compact && typeof payload.turnId === "string" && payload.turnId) this.messageStatus(turnId, "started");
11222
11284
  this.setState("WORKING");
11223
- if (!compact) await this.checkpointTurn(turnId);
11285
+ let checkpointMs = null;
11286
+ const saved = compact ? Promise.resolve() : this.checkpointTurn(turnId).then(() => {
11287
+ checkpointMs = Date.now() - turnStart;
11288
+ }, (e) => {
11289
+ this.deps.log.warn("turn checkpoint failed", { error: this.redactor.text(e.message) });
11290
+ });
11291
+ this.toolGate = saved;
11224
11292
  if (!compact) {
11225
11293
  await this.refreshSkills();
11294
+ if (!this.local) await saved;
11226
11295
  this.startEnvSetups();
11227
11296
  await Promise.all(this.envSetups.values());
11228
11297
  }
@@ -11320,6 +11389,9 @@ ${text3}`;
11320
11389
  timing: new TurnTiming({ sessionId: this.deps.sessionId ?? null, turnId, model, reasoning: this.reasoningLevel }, typeof payload.receivedAt === "number" ? payload.receivedAt : null, turnStart)
11321
11390
  };
11322
11391
  this.live = live;
11392
+ void saved.then(() => {
11393
+ if (checkpointMs !== null) live.timing.phases({ checkpointMs });
11394
+ });
11323
11395
  const attempt = async (resume, at) => {
11324
11396
  let sawInit = false;
11325
11397
  let conversation = resume;
@@ -11356,41 +11428,26 @@ ${text3}`;
11356
11428
  live.turnEnded = false;
11357
11429
  live.headResult = null;
11358
11430
  channel.start(userMessage(turnPrompt, context.images, messageUuid(turnId)));
11359
- const options = buildQueryOptions({
11360
- root: this.deps.root,
11361
- model,
11362
- resume,
11363
- resumeAt: resume ? at : null,
11364
- abortController: ac,
11365
- reasoning: reasoning.options,
11366
- env: buildEngineEnv(boot, this.deps.configDir, model, { local: this.local, reasoning, output, extraEnv: this.projectEnv(), plan: planMode }),
11367
- mcpServer: this.mcpServer,
11368
- systemAppend: await this.systemAppend(),
11369
- policy: {
11370
- root: this.deps.root,
11371
- extraReadRoots: [this.deps.configDir],
11372
- deniedRoots: this.deps.privateDirs,
11373
- local: this.local,
11374
- plan: planMode,
11375
- planDir: (0, import_path15.join)(this.deps.configDir, "plans"),
11376
- orgMcpTools: orgMcpToolNames(Object.keys(off.mcpServers).length ? off.mcpCatalog : null)
11377
- },
11378
- plan: planMode,
11379
- onPlan: (text3) => this.proposePlan(live.turnId, text3),
11380
- skills: { dir: this.skillsDir(), enabled: enabledSkillNames(off.skillEntries), pluginDirs: off.pluginDirs },
11381
- ...Object.keys(off.mcpServers).length ? { orgMcpServers: off.mcpServers } : {},
11382
- pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
11383
- commandGate: this.deps.commandGate,
11384
- ...this.localFolder?.kind === "folder" ? { beforeWrite: (path) => this.beforeFolderWrite(path) } : {}
11385
- });
11431
+ const inputs = await this.engineInputs({ model, reasoning, output, planMode, off, resume, resumeAt: resume ? at : null });
11386
11432
  if (!streaming) channel.close();
11387
11433
  const single = (text3) => context.images.length ? (async function* () {
11388
11434
  yield userMessage(text3, context.images);
11389
11435
  })() : text3;
11390
- const q = sdk.query({ prompt: streaming ? channel : single(turnPrompt), options });
11436
+ const warm = streaming ? await this.claimWarm(spawnKey(inputs)) : (this.dropWarm(), null);
11437
+ let q;
11438
+ if (warm) {
11439
+ const forward = () => warm.abort.abort();
11440
+ if (ac.signal.aborted) forward();
11441
+ else ac.signal.addEventListener("abort", forward, { once: true });
11442
+ q = warm.handle.query(channel);
11443
+ } else {
11444
+ q = sdk.query({ prompt: streaming ? channel : single(turnPrompt), options: this.queryOptions(inputs, ac, this.mcpServer) });
11445
+ }
11446
+ live.timing.phases({ prepareMs: Date.now() - turnStart, warmStart: !!warm });
11391
11447
  live.query = q;
11392
11448
  try {
11393
11449
  for await (const msg of q) {
11450
+ live.timing.engineFrame();
11394
11451
  const f = msg && typeof msg === "object" ? msg : {};
11395
11452
  const main2 = !f.parent_tool_use_id;
11396
11453
  if (f.type === "command_lifecycle") {
@@ -11445,6 +11502,7 @@ ${text3}`;
11445
11502
  }
11446
11503
  } finally {
11447
11504
  const stopped = ac.signal.aborted || live.interrupted;
11505
+ await saved;
11448
11506
  live.timing.end(this.deps.log, stopped ? "stopped" : "failed");
11449
11507
  this.live = null;
11450
11508
  this.turnAbort = null;
@@ -11585,6 +11643,7 @@ ${text3}`;
11585
11643
  }
11586
11644
  live.between = true;
11587
11645
  live.snapshot = this.takeSnapshot();
11646
+ this.toolGate = live.snapshot.then(() => void 0, () => void 0);
11588
11647
  }
11589
11648
  /** The query ended: its input closes, and a message it never read runs as the next turn (or is dropped by a rewind). */
11590
11649
  settleLive(live) {
@@ -11625,6 +11684,146 @@ ${text3}`;
11625
11684
  output: { limit: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null, ceiling: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null }
11626
11685
  };
11627
11686
  }
11687
+ /** The data a turn's engine process starts with (see EngineInputs). */
11688
+ async engineInputs(t) {
11689
+ const boot = this.boot;
11690
+ const org = Object.keys(t.off.mcpServers).length > 0;
11691
+ return {
11692
+ model: t.model,
11693
+ resume: t.resume,
11694
+ resumeAt: t.resumeAt,
11695
+ reasoning: t.reasoning.options,
11696
+ env: buildEngineEnv(boot, this.deps.configDir, t.model, { local: this.local, reasoning: t.reasoning, output: t.output, extraEnv: this.projectEnv(), plan: t.planMode }),
11697
+ systemAppend: await this.systemAppend(),
11698
+ policy: {
11699
+ root: this.deps.root,
11700
+ extraReadRoots: [this.deps.configDir],
11701
+ deniedRoots: this.deps.privateDirs,
11702
+ local: this.local,
11703
+ plan: t.planMode,
11704
+ planDir: (0, import_path15.join)(this.deps.configDir, "plans"),
11705
+ orgMcpTools: orgMcpToolNames(org ? t.off.mcpCatalog : null)
11706
+ },
11707
+ plan: t.planMode,
11708
+ skills: { dir: this.skillsDir(), enabled: enabledSkillNames(t.off.skillEntries), pluginDirs: t.off.pluginDirs },
11709
+ skillsKey: this.skillsSent,
11710
+ orgMcpServers: org ? t.off.mcpServers : {},
11711
+ folderWrites: this.localFolder?.kind === "folder"
11712
+ };
11713
+ }
11714
+ /**
11715
+ * The query options of a turn's engine process. The callbacks read the engine's current turn (never one captured
11716
+ * here), so a process started ahead serves the turn that takes it: its plan goes to that turn, and its tools wait
11717
+ * for that turn's saved point.
11718
+ */
11719
+ queryOptions(i, abortController, mcpServer) {
11720
+ const gate = this.deps.commandGate;
11721
+ return buildQueryOptions({
11722
+ root: this.deps.root,
11723
+ model: i.model,
11724
+ resume: i.resume,
11725
+ resumeAt: i.resumeAt,
11726
+ abortController,
11727
+ reasoning: i.reasoning,
11728
+ env: i.env,
11729
+ mcpServer,
11730
+ systemAppend: i.systemAppend,
11731
+ policy: i.policy,
11732
+ plan: i.plan,
11733
+ onPlan: (text3) => {
11734
+ const live = this.live;
11735
+ if (live) this.proposePlan(live.turnId, text3);
11736
+ },
11737
+ skills: i.skills,
11738
+ ...Object.keys(i.orgMcpServers).length ? { orgMcpServers: i.orgMcpServers } : {},
11739
+ pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
11740
+ commandGate: gate ? timedGate(gate, (ms) => this.live?.timing.permission(ms)) : void 0,
11741
+ beforeTool: async () => {
11742
+ const t0 = Date.now();
11743
+ await this.toolGate;
11744
+ const waited = Date.now() - t0;
11745
+ if (waited > 0) this.live?.timing.toolWait(waited);
11746
+ },
11747
+ ...i.folderWrites ? { beforeWrite: (path) => this.beforeFolderWrite(path) } : {}
11748
+ });
11749
+ }
11750
+ /**
11751
+ * The inputs of the next turn as the person would most likely send it (same model, reasoning, mode and tools as the
11752
+ * session now, an everyday task), for the process started ahead. null when that process could not serve it safely:
11753
+ * the organization's MCP servers are in-process objects shared by every process, and one serves one process at a time.
11754
+ */
11755
+ async nextTurnInputs() {
11756
+ const boot = this.boot;
11757
+ const off = this.turnOff(void 0);
11758
+ if (Object.keys(off.mcpServers).length) return null;
11759
+ const primary = boot.runtime.primaryModel || boot.model;
11760
+ const route = routeAutoTurn(boot, {
11761
+ content: "",
11762
+ previousTrouble: this.previousTrouble,
11763
+ maxMode: this.reasoningLevel === "max" || isMaxMode(this.reasoningLevel, this.reasoningCapability)
11764
+ });
11765
+ const { model, reasoning, output } = this.turnModel(primary, route);
11766
+ const canResume = !!this.sdkSessionId && (this.knownSessions.has(this.sdkSessionId) || await hasLocalTranscript(this.deps.configDir, this.sdkSessionId));
11767
+ return this.engineInputs({ model, reasoning, output, planMode: false, off, resume: canResume ? this.sdkSessionId : null, resumeAt: canResume ? this.resumeAt : null });
11768
+ }
11769
+ /**
11770
+ * Starts the engine process of the next turn now (warmStart), while the session waits for the person: Claude Code
11771
+ * keeps its process up between messages, and here the next message then goes to a ready process. Only when nothing
11772
+ * runs or waits; a process already started with the same inputs is kept.
11773
+ */
11774
+ prewarm() {
11775
+ const sdk = this.sdk;
11776
+ if (!this.deps.warmStart || !sdk?.startup || !this.boot || this.stopping || this.turnRunning || this.queue.length || this.prewarming) return;
11777
+ this.prewarming = (async () => {
11778
+ const inputs = await this.nextTurnInputs();
11779
+ if (!inputs || this.stopping || this.turnRunning || this.queue.length) return;
11780
+ const key = spawnKey(inputs);
11781
+ if (this.warm?.key === key) return;
11782
+ this.dropWarm();
11783
+ const abort = new AbortController();
11784
+ const server = buildScaleQualityServer(sdk, this.toolHost());
11785
+ const ready = sdk.startup({ options: this.queryOptions(inputs, abort, server) }).catch((e) => {
11786
+ this.deps.log.warn("engine warm start failed", { error: this.redactor.text(e.message) });
11787
+ return null;
11788
+ });
11789
+ const timer = setTimeout(() => {
11790
+ if (this.warm?.abort === abort) this.dropWarm();
11791
+ }, this.deps.warmIdleMs ?? WARM_IDLE_MS);
11792
+ timer.unref?.();
11793
+ this.warm = { key, abort, ready, timer };
11794
+ })().catch((e) => {
11795
+ this.deps.log.warn("engine warm start failed", { error: this.redactor.text(e.message) });
11796
+ }).finally(() => {
11797
+ this.prewarming = null;
11798
+ });
11799
+ }
11800
+ /** The process started ahead, for a turn with this start key; any other is closed (the turn starts its own). */
11801
+ async claimWarm(key) {
11802
+ await this.prewarming;
11803
+ const w = this.warm;
11804
+ if (!w) return null;
11805
+ if (w.key !== key) {
11806
+ this.dropWarm();
11807
+ return null;
11808
+ }
11809
+ this.warm = null;
11810
+ clearTimeout(w.timer);
11811
+ const handle = await w.ready;
11812
+ return handle ? { handle, abort: w.abort } : null;
11813
+ }
11814
+ /** Closes the process started ahead, if any. */
11815
+ dropWarm() {
11816
+ const w = this.warm;
11817
+ if (!w) return;
11818
+ this.warm = null;
11819
+ clearTimeout(w.timer);
11820
+ void w.ready.then((h) => {
11821
+ try {
11822
+ h?.close();
11823
+ } catch {
11824
+ }
11825
+ }).finally(() => w.abort.abort());
11826
+ }
11628
11827
  /** The imported conversation as a context block (fetched once); null when there is none or it cannot be read. */
11629
11828
  importedHistoryBlock() {
11630
11829
  const info = this.boot?.imported;
@@ -11736,6 +11935,7 @@ ${patch}`;
11736
11935
  if (this.stopping) return;
11737
11936
  this.stopping = true;
11738
11937
  this.queue.length = 0;
11938
+ this.dropWarm();
11739
11939
  this.turnAbort?.abort();
11740
11940
  this.pollAbort.abort();
11741
11941
  this.envAbort.abort();
@@ -12261,6 +12461,7 @@ function buildQueryOptions(o) {
12261
12461
  };
12262
12462
  const preToolUse = async (input) => {
12263
12463
  if (input.hook_event_name !== "PreToolUse") return {};
12464
+ if (o.beforeTool) await o.beforeTool();
12264
12465
  const name = String(input.tool_name ?? "");
12265
12466
  if (name === "ExitPlanMode") return o.plan ? {} : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Not in plan mode." } };
12266
12467
  const provenance = input.mcp_server;
@@ -12276,6 +12477,7 @@ function buildQueryOptions(o) {
12276
12477
  return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
12277
12478
  };
12278
12479
  const canUseTool = async (toolName, input, opts) => {
12480
+ if (o.beforeTool) await o.beforeTool();
12279
12481
  if (toolName.startsWith("mcp__") && opts?.mcpServer?.source && opts.mcpServer.source !== "sdk") {
12280
12482
  return { behavior: "deny", message: "Only ScaleQuality tools are available in this workspace." };
12281
12483
  }
@@ -12337,6 +12539,24 @@ function buildQueryOptions(o) {
12337
12539
  ...o.pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable: o.pathToClaudeCodeExecutable } : {}
12338
12540
  };
12339
12541
  }
12542
+ function spawnKey(i) {
12543
+ const { orgMcpServers, ...data } = i;
12544
+ return (0, import_crypto7.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
12545
+ }
12546
+ function timedGate(gate, took) {
12547
+ const timed = async (run2) => {
12548
+ const t0 = Date.now();
12549
+ try {
12550
+ return await run2();
12551
+ } finally {
12552
+ took(Date.now() - t0);
12553
+ }
12554
+ };
12555
+ return {
12556
+ check: (command, opts) => timed(() => gate.check(command, opts)),
12557
+ ...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {}
12558
+ };
12559
+ }
12340
12560
  async function hasLocalTranscript(configDir, sessionId) {
12341
12561
  if (!/^[A-Za-z0-9-]{8,80}$/.test(sessionId)) return false;
12342
12562
  const projects = (0, import_path15.join)(configDir, "projects");
@@ -12390,6 +12610,10 @@ function createLocalEngine(o) {
12390
12610
  const log = {
12391
12611
  info: (msg, ctx) => {
12392
12612
  if (o.verbose) say(style.dim(`${o.prefix ?? ""}[info] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
12613
+ else if (msg === "WORKSPACE_TURN_TIMING") {
12614
+ const line = turnTimingLine(ctx);
12615
+ if (line) say(`${o.prefix ?? ""}${style.dim(` ${line}`)}`);
12616
+ }
12393
12617
  },
12394
12618
  warn: (msg, ctx) => {
12395
12619
  if (o.verbose) say(style.dim(`${o.prefix ?? ""}[warn] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
@@ -12465,7 +12689,9 @@ function createLocalEngine(o) {
12465
12689
  // An imported Claude Code conversation of this computer and folder resumes from its own transcript.
12466
12690
  resumeImported: (externalId) => copyClaudeTranscript(sources, externalId, o.root, configDir),
12467
12691
  exit: (code, reason) => o.exit(code, reason, startError),
12468
- pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE || void 0
12692
+ pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE || void 0,
12693
+ // The next message's engine process is started while the person reads, like Claude Code's process between messages.
12694
+ warmStart: true
12469
12695
  });
12470
12696
  }
12471
12697
  async function connectMain(argv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.9-rc.1",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {