scream-code 0.6.7 → 0.6.9

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.
@@ -43,10 +43,11 @@ import * as z4mini from "zod/v4-mini";
43
43
  import process$1 from "node:process";
44
44
  import { AsyncLocalStorage } from "node:async_hooks";
45
45
  import { Command, Option } from "commander";
46
- import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
46
+ import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
47
47
  import chalk, { chalkStderr } from "chalk";
48
48
  import { createInterface } from "node:readline/promises";
49
49
  import { highlight, supportsLanguage } from "cli-highlight";
50
+ import { diffWords } from "diff";
50
51
  import { gt, valid } from "semver";
51
52
  import { createInterface as createInterface$1 } from "node:readline";
52
53
  //#region ../../node_modules/.pnpm/pathe@2.0.3/node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
@@ -731,6 +732,49 @@ function createToolMessage(toolCallId, output) {
731
732
  };
732
733
  }
733
734
  //#endregion
735
+ //#region ../../packages/ltod/src/rate-limit-utils.ts
736
+ const QUOTA_EXHAUSTED_BACKOFF_MS = 1800 * 1e3;
737
+ const RATE_LIMIT_EXCEEDED_BACKOFF_MS = 30 * 1e3;
738
+ const MODEL_CAPACITY_BASE_MS = 45 * 1e3;
739
+ const MODEL_CAPACITY_JITTER_MS = 30 * 1e3;
740
+ const SERVER_ERROR_BACKOFF_MS = 20 * 1e3;
741
+ const ACCOUNT_RATE_LIMIT_PATTERN = /\baccount(?:'s)?\b[^\n]{0,80}\brate.?limit\b|\brate.?limit\b[^\n]{0,80}\baccount\b/i;
742
+ const INSUFFICIENT_BALANCE_PATTERN = /insufficient.?balance/i;
743
+ /**
744
+ * Classify a rate-limit error message into a reason category.
745
+ *
746
+ * Priority: QUOTA (explicit "quota will reset") > MODEL_CAPACITY > QUOTA (account)
747
+ * > RATE_LIMIT > QUOTA (generic) > SERVER_ERROR > UNKNOWN.
748
+ *
749
+ * "quota will reset" / "exhausted your capacity" short-circuits to QUOTA before
750
+ * the MODEL_CAPACITY fallthrough — the word "capacity" appears in both but the
751
+ * long-wait signal means credential rotation, not a 60s backoff.
752
+ */
753
+ function parseRateLimitReason(errorMessage) {
754
+ const lower = errorMessage.toLowerCase();
755
+ if (lower.includes("quota will reset") || lower.includes("exhausted your capacity")) return "QUOTA_EXHAUSTED";
756
+ if (lower.includes("capacity") || lower.includes("overloaded") || lower.includes("529") || lower.includes("503") || lower.includes("resource exhausted")) return "MODEL_CAPACITY_EXHAUSTED";
757
+ if (ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage)) return "QUOTA_EXHAUSTED";
758
+ if (lower.includes("per minute") || lower.includes("rate limit") || lower.includes("too many requests")) return "RATE_LIMIT_EXCEEDED";
759
+ if (lower.includes("exhausted") || lower.includes("quota") || lower.includes("usage limit") || INSUFFICIENT_BALANCE_PATTERN.test(errorMessage)) return "QUOTA_EXHAUSTED";
760
+ if (lower.includes("500") || lower.includes("internal error") || lower.includes("internal server error")) return "SERVER_ERROR";
761
+ return "UNKNOWN";
762
+ }
763
+ /**
764
+ * Backoff in ms for a given reason. MODEL_CAPACITY gets jitter to avoid
765
+ * thundering herd. UNKNOWN falls back to the conservative QUOTA backoff
766
+ * — safer to wait too long than to hammer a rate-limited endpoint.
767
+ */
768
+ function calculateRateLimitBackoffMs(reason) {
769
+ switch (reason) {
770
+ case "QUOTA_EXHAUSTED": return QUOTA_EXHAUSTED_BACKOFF_MS;
771
+ case "RATE_LIMIT_EXCEEDED": return RATE_LIMIT_EXCEEDED_BACKOFF_MS;
772
+ case "MODEL_CAPACITY_EXHAUSTED": return MODEL_CAPACITY_BASE_MS + Math.random() * MODEL_CAPACITY_JITTER_MS;
773
+ case "SERVER_ERROR": return SERVER_ERROR_BACKOFF_MS;
774
+ default: return QUOTA_EXHAUSTED_BACKOFF_MS;
775
+ }
776
+ }
777
+ //#endregion
734
778
  //#region ../../packages/ltod/src/errors.ts
735
779
  /**
736
780
  * Base error for all chat provider errors.
@@ -784,12 +828,16 @@ var APIContextOverflowError = class extends APIStatusError {
784
828
  };
785
829
  /**
786
830
  * HTTP status error that specifically means the provider rate-limited the
787
- * request.
831
+ * request. Carries a `reason` so retry logic can pick the right backoff
832
+ * (quota exhaustion needs 30min or a credential switch; a transient 529
833
+ * just needs 45s) instead of treating every 429 the same.
788
834
  */
789
835
  var APIProviderRateLimitError = class extends APIStatusError {
790
- constructor(message, requestId) {
836
+ reason;
837
+ constructor(message, requestId, reason) {
791
838
  super(429, message, requestId);
792
839
  this.name = "APIProviderRateLimitError";
840
+ this.reason = reason ?? "UNKNOWN";
793
841
  }
794
842
  };
795
843
  /**
@@ -804,6 +852,7 @@ var APIEmptyResponseError = class extends ChatProviderError {
804
852
  function isRetryableGenerateError(error) {
805
853
  if (error instanceof APIConnectionError$3 || error instanceof APITimeoutError) return true;
806
854
  if (error instanceof APIEmptyResponseError) return true;
855
+ if (error instanceof APIProviderRateLimitError) return error.reason !== "QUOTA_EXHAUSTED";
807
856
  return error instanceof APIStatusError && [
808
857
  429,
809
858
  500,
@@ -826,7 +875,7 @@ function isContextOverflowErrorCode(code) {
826
875
  return code === "context_length_exceeded";
827
876
  }
828
877
  function normalizeAPIStatusError(statusCode, message, requestId) {
829
- if (statusCode === 429) return new APIProviderRateLimitError(message, requestId);
878
+ if (statusCode === 429) return new APIProviderRateLimitError(message, requestId, parseRateLimitReason(message));
830
879
  if (isContextOverflowStatusError(statusCode, message)) return new APIContextOverflowError(statusCode, message, requestId);
831
880
  return new APIStatusError(statusCode, message, requestId);
832
881
  }
@@ -8951,6 +9000,7 @@ function convertAnthropicError(error) {
8951
9000
  const reqId = error.requestID ?? null;
8952
9001
  return normalizeAPIStatusError(error.status, error.message, reqId);
8953
9002
  }
9003
+ if (error instanceof APIUserAbortError$2) throw new DOMException("The operation was aborted.", "AbortError");
8954
9004
  if (error instanceof AnthropicError) return new ChatProviderError(`Anthropic error: ${error.message}`);
8955
9005
  if (error instanceof Error) return new ChatProviderError(`Error: ${error.message}`);
8956
9006
  return new ChatProviderError(`Error: ${String(error)}`);
@@ -9079,7 +9129,7 @@ var AnthropicStreamedMessage = class {
9079
9129
  type: "function",
9080
9130
  id: block.id,
9081
9131
  name: block.name,
9082
- arguments: "",
9132
+ arguments: null,
9083
9133
  _streamIndex: blockIndex
9084
9134
  };
9085
9135
  break;
@@ -48130,6 +48180,7 @@ function convertOpenAIError(error) {
48130
48180
  return normalizeAPIStatusError(error.status, error.message, reqId);
48131
48181
  }
48132
48182
  if (error instanceof APIError && error.constructor === APIError && error.error === void 0) return classifyBaseApiError(error.message);
48183
+ if (error instanceof APIUserAbortError) throw new DOMException("The operation was aborted.", "AbortError");
48133
48184
  if (error instanceof OpenAIError) return new ChatProviderError(`Error: ${error.message}`);
48134
48185
  if (error instanceof Error) return classifyBaseApiError(error.message);
48135
48186
  return new ChatProviderError(`Error: ${String(error)}`);
@@ -48187,7 +48238,7 @@ function extractUsage(usage) {
48187
48238
  if (typeof details["cached_tokens"] === "number") cached = details["cached_tokens"];
48188
48239
  }
48189
48240
  return {
48190
- inputOther: promptTokens - cached,
48241
+ inputOther: Math.max(0, promptTokens - cached),
48191
48242
  output: completionTokens,
48192
48243
  inputCacheRead: cached,
48193
48244
  inputCacheCreation: 0
@@ -48426,6 +48477,10 @@ function convertChatCompletionStreamToolCall(toolCall, bufferedByIndex) {
48426
48477
  }
48427
48478
  //#endregion
48428
48479
  //#region ../../packages/ltod/src/providers/scream.ts
48480
+ const OPENAI_CHAT_TOOL_CALL_ID_POLICY$1 = {
48481
+ normalize: (id) => sanitizeToolCallId(id, 64),
48482
+ maxLength: 64
48483
+ };
48429
48484
  function isEffectivelyEmptyContent(parts) {
48430
48485
  for (const part of parts) {
48431
48486
  if (part.type !== "text") return false;
@@ -48632,12 +48687,13 @@ var ScreamChatProvider = class {
48632
48687
  };
48633
48688
  }
48634
48689
  async generate(systemPrompt, tools, history, options) {
48690
+ const normalizedHistory = normalizeToolCallIdsForProvider(history, OPENAI_CHAT_TOOL_CALL_ID_POLICY$1);
48635
48691
  const messages = [];
48636
48692
  if (systemPrompt) messages.push({
48637
48693
  role: "system",
48638
48694
  content: systemPrompt
48639
48695
  });
48640
- for (const msg of history) messages.push(convertMessage$2(msg));
48696
+ for (const msg of normalizedHistory) messages.push(convertMessage$2(msg));
48641
48697
  const kwargs = { ...this._generationKwargs };
48642
48698
  for (const key of Object.keys(kwargs)) if (kwargs[key] === void 0) delete kwargs[key];
48643
48699
  if (kwargs["max_completion_tokens"] === void 0 && kwargs["max_tokens"] !== void 0) kwargs["max_completion_tokens"] = kwargs["max_tokens"];
@@ -49324,7 +49380,7 @@ var OpenAIResponsesStreamedMessage = class {
49324
49380
  const details = readObjectField(usage, "input_tokens_details");
49325
49381
  const cached = details ? readNumberField(details, "cached_tokens") ?? 0 : 0;
49326
49382
  this._usage = {
49327
- inputOther: inputTokens - cached,
49383
+ inputOther: Math.max(0, inputTokens - cached),
49328
49384
  output: outputTokens,
49329
49385
  inputCacheRead: cached,
49330
49386
  inputCacheCreation: 0
@@ -49718,6 +49774,227 @@ function catalogProviderModels(entry) {
49718
49774
  return Object.values(models).map((model) => catalogModelToCapability(model)).filter((model) => model !== void 0);
49719
49775
  }
49720
49776
  //#endregion
49777
+ //#region ../../packages/ltod/src/idle-iterator.ts
49778
+ /**
49779
+ * Idle-stream watchdog.
49780
+ *
49781
+ * Ported from oh-my-pi `packages/ai/src/utils/idle-iterator.ts:154-382`.
49782
+ * Reasoning models (deepseek-reasoner, mimo thinking) can sit silent for
49783
+ * 30s+ between tokens; without a watchdog the stream hangs forever and the
49784
+ * user thinks the agent died. This wraps any async iterable with a per-item
49785
+ * idle deadline, turning a stalled stream into a retryable `APITimeoutError`.
49786
+ *
49787
+ * Simplified from the upstream version: no `armPreResponseTimeout`, no
49788
+ * `iterateWithTerminalGrace`, no per-provider env-var aliases. The core
49789
+ * racer-reuse and single-timer-self-rearm design is preserved verbatim.
49790
+ */
49791
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 6e4;
49792
+ const DEFAULT_STREAM_FIRST_ITEM_TIMEOUT_MS = 12e4;
49793
+ const RACER_REMINT_INTERVAL = 1024;
49794
+ function withResolvers() {
49795
+ let resolve;
49796
+ let reject;
49797
+ return {
49798
+ promise: new Promise((res, rej) => {
49799
+ resolve = res;
49800
+ reject = rej;
49801
+ }),
49802
+ resolve,
49803
+ reject
49804
+ };
49805
+ }
49806
+ function normalizeTimeoutMs$1(value, fallback) {
49807
+ if (value === void 0) return fallback;
49808
+ const parsed = Number(value);
49809
+ if (!Number.isFinite(parsed)) return fallback;
49810
+ if (parsed <= 0) return void 0;
49811
+ return Math.trunc(parsed);
49812
+ }
49813
+ function getStreamIdleTimeoutMs(fallback = DEFAULT_STREAM_IDLE_TIMEOUT_MS) {
49814
+ return normalizeTimeoutMs$1(process.env["SCREAM_STREAM_IDLE_TIMEOUT_MS"], fallback);
49815
+ }
49816
+ function getStreamFirstItemTimeoutMs(idleTimeoutMs, fallback = DEFAULT_STREAM_FIRST_ITEM_TIMEOUT_MS) {
49817
+ const floor = idleTimeoutMs === void 0 ? fallback : Math.max(fallback, idleTimeoutMs);
49818
+ return normalizeTimeoutMs$1(process.env["SCREAM_STREAM_FIRST_ITEM_TIMEOUT_MS"], floor);
49819
+ }
49820
+ /**
49821
+ * Thrown when the stream stalls longer than the configured idle deadline.
49822
+ * Extends `APITimeoutError` so `isRetryableGenerateError` picks it up
49823
+ * automatically — the agent retries instead of dying.
49824
+ */
49825
+ var StreamIdleTimeoutError = class extends APITimeoutError {
49826
+ phase;
49827
+ constructor(phase, message) {
49828
+ super(message);
49829
+ this.name = "StreamIdleTimeoutError";
49830
+ this.phase = phase;
49831
+ }
49832
+ };
49833
+ /**
49834
+ * Yields items from an async iterable while enforcing a maximum idle gap.
49835
+ *
49836
+ * The first item may use a longer timeout (reasoning models can take 60s+ to
49837
+ * emit the first token); subsequent gaps use the shorter idle timeout.
49838
+ */
49839
+ async function* iterateWithIdleTimeout(iterable, options) {
49840
+ const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs;
49841
+ const firstItemDeadlineMs = firstItemTimeoutMs !== void 0 && firstItemTimeoutMs > 0 ? Date.now() + firstItemTimeoutMs : void 0;
49842
+ const abortSignal = options.abortSignal;
49843
+ const iterator = iterable[Symbol.asyncIterator]();
49844
+ let iteratorClosed = false;
49845
+ const closeIterator = () => {
49846
+ if (iteratorClosed) return;
49847
+ iteratorClosed = true;
49848
+ const returnPromise = iterator.return?.();
49849
+ if (returnPromise) returnPromise.catch(() => {});
49850
+ };
49851
+ if (abortSignal?.aborted) {
49852
+ closeIterator();
49853
+ throw abortReason(abortSignal);
49854
+ }
49855
+ const withRacy = (promise) => promise.then((result) => ({
49856
+ kind: "next",
49857
+ result
49858
+ }), (error) => ({
49859
+ kind: "error",
49860
+ error
49861
+ }));
49862
+ let awaitingFirstItem = true;
49863
+ const isProgressItem = (item) => {
49864
+ if (!options.isProgressItem) return true;
49865
+ try {
49866
+ return options.isProgressItem(item);
49867
+ } catch {
49868
+ return true;
49869
+ }
49870
+ };
49871
+ let lastProgressAt = Date.now();
49872
+ const noTimeoutEnforced = (firstItemTimeoutMs === void 0 || firstItemTimeoutMs <= 0) && (options.idleTimeoutMs === void 0 || options.idleTimeoutMs <= 0);
49873
+ let abortPromise;
49874
+ let abortListener;
49875
+ let resolveAbort;
49876
+ if (abortSignal) {
49877
+ const { promise, resolve } = withResolvers();
49878
+ resolveAbort = resolve;
49879
+ abortListener = () => resolveAbort?.({ kind: "abort" });
49880
+ abortSignal.addEventListener("abort", abortListener, { once: true });
49881
+ abortPromise = promise;
49882
+ }
49883
+ let timeoutPromise;
49884
+ let resolveTimeout;
49885
+ let timeoutFired = false;
49886
+ let timer;
49887
+ let timerFireAtMs = Infinity;
49888
+ const currentDeadlineMs = () => {
49889
+ if (awaitingFirstItem) return firstItemDeadlineMs;
49890
+ if (options.idleTimeoutMs !== void 0 && options.idleTimeoutMs > 0) return lastProgressAt + options.idleTimeoutMs;
49891
+ };
49892
+ const onTimerFire = () => {
49893
+ timer = void 0;
49894
+ timerFireAtMs = Infinity;
49895
+ const deadlineMs = currentDeadlineMs();
49896
+ if (deadlineMs === void 0) return;
49897
+ const remainingMs = deadlineMs - Date.now();
49898
+ if (remainingMs > 0) {
49899
+ timerFireAtMs = deadlineMs;
49900
+ timer = setTimeout(onTimerFire, remainingMs);
49901
+ return;
49902
+ }
49903
+ timeoutFired = true;
49904
+ resolveTimeout?.({ kind: "timeout" });
49905
+ };
49906
+ const armTimer = (deadlineMs) => {
49907
+ if (timeoutPromise === void 0 || timeoutFired) {
49908
+ const { promise, resolve } = withResolvers();
49909
+ timeoutPromise = promise;
49910
+ resolveTimeout = resolve;
49911
+ timeoutFired = false;
49912
+ }
49913
+ if (timer !== void 0) {
49914
+ if (timerFireAtMs <= deadlineMs) return;
49915
+ clearTimeout(timer);
49916
+ }
49917
+ timerFireAtMs = deadlineMs;
49918
+ timer = setTimeout(onTimerFire, Math.max(0, deadlineMs - Date.now()));
49919
+ };
49920
+ try {
49921
+ let raceCount = 0;
49922
+ while (true) {
49923
+ if (++raceCount % RACER_REMINT_INTERVAL === 0) {
49924
+ if (abortPromise !== void 0 && !abortSignal.aborted) {
49925
+ const { promise, resolve } = withResolvers();
49926
+ resolveAbort = resolve;
49927
+ abortPromise = promise;
49928
+ }
49929
+ if (timeoutPromise !== void 0 && !timeoutFired) {
49930
+ const { promise, resolve } = withResolvers();
49931
+ resolveTimeout = resolve;
49932
+ timeoutPromise = promise;
49933
+ }
49934
+ }
49935
+ let activeTimeoutMs;
49936
+ if (awaitingFirstItem) {
49937
+ if (firstItemDeadlineMs !== void 0) {
49938
+ activeTimeoutMs = firstItemDeadlineMs - Date.now();
49939
+ if (activeTimeoutMs <= 0) {
49940
+ closeIterator();
49941
+ throw new StreamIdleTimeoutError("first-item", options.firstItemErrorMessage ?? options.errorMessage);
49942
+ }
49943
+ }
49944
+ } else if (options.idleTimeoutMs !== void 0 && options.idleTimeoutMs > 0) {
49945
+ activeTimeoutMs = options.idleTimeoutMs - (Date.now() - lastProgressAt);
49946
+ if (activeTimeoutMs <= 0) {
49947
+ closeIterator();
49948
+ throw new StreamIdleTimeoutError("idle", options.errorMessage);
49949
+ }
49950
+ }
49951
+ const racers = [withRacy(iterator.next())];
49952
+ if (!noTimeoutEnforced && activeTimeoutMs !== void 0 && activeTimeoutMs > 0 && activeTimeoutMs !== void 0) {
49953
+ armTimer(Date.now() + activeTimeoutMs);
49954
+ racers.push(timeoutPromise);
49955
+ }
49956
+ if (abortPromise) racers.push(abortPromise);
49957
+ let continuing = false;
49958
+ try {
49959
+ const outcome = await Promise.race(racers);
49960
+ if (outcome.kind === "abort") {
49961
+ closeIterator();
49962
+ throw abortReason(abortSignal);
49963
+ }
49964
+ if (outcome.kind === "timeout") {
49965
+ closeIterator();
49966
+ throw new StreamIdleTimeoutError(!awaitingFirstItem ? "idle" : "first-item", !awaitingFirstItem ? options.errorMessage : options.firstItemErrorMessage ?? options.errorMessage);
49967
+ }
49968
+ if (outcome.kind === "error") throw outcome.error;
49969
+ if (outcome.result.done) {
49970
+ awaitingFirstItem = false;
49971
+ return;
49972
+ }
49973
+ const item = outcome.result.value;
49974
+ if (isProgressItem(item)) {
49975
+ awaitingFirstItem = false;
49976
+ lastProgressAt = Date.now();
49977
+ }
49978
+ yield item;
49979
+ continuing = true;
49980
+ } finally {
49981
+ if (!continuing) closeIterator();
49982
+ }
49983
+ }
49984
+ } finally {
49985
+ if (timer !== void 0) clearTimeout(timer);
49986
+ resolveTimeout?.({ kind: "timeout" });
49987
+ if (abortListener && abortSignal) abortSignal.removeEventListener("abort", abortListener);
49988
+ resolveAbort?.({ kind: "abort" });
49989
+ }
49990
+ }
49991
+ function abortReason(signal) {
49992
+ const reason = signal.reason;
49993
+ if (reason instanceof Error) return reason;
49994
+ if (typeof reason === "string") return new Error(reason);
49995
+ return /* @__PURE__ */ new Error("Request was aborted");
49996
+ }
49997
+ //#endregion
49721
49998
  //#region ../../packages/ltod/src/generate.ts
49722
49999
  /**
49723
50000
  * Generate one assistant message by streaming from the given provider.
@@ -49755,7 +50032,16 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
49755
50032
  options?.onRequestStart?.();
49756
50033
  const stream = await provider.generate(systemPrompt, tools, history, options);
49757
50034
  await throwIfAborted(options?.signal, stream);
49758
- for await (const part of stream) {
50035
+ const idleTimeoutMs = getStreamIdleTimeoutMs();
50036
+ const firstItemTimeoutMs = getStreamFirstItemTimeoutMs(idleTimeoutMs);
50037
+ const watchedStream = iterateWithIdleTimeout(stream, {
50038
+ idleTimeoutMs,
50039
+ firstItemTimeoutMs,
50040
+ abortSignal: options?.signal,
50041
+ errorMessage: `Stream stalled — no data for ${idleTimeoutMs ?? 0}ms. Provider: ${provider.name}, model: ${provider.modelName}`,
50042
+ firstItemErrorMessage: `Stream stalled — no first token within ${firstItemTimeoutMs ?? 0}ms. Provider: ${provider.name}, model: ${provider.modelName}`
50043
+ });
50044
+ for await (const part of watchedStream) {
49759
50045
  await throwIfAborted(options?.signal, stream);
49760
50046
  if (callbacks?.onMessagePart !== void 0) {
49761
50047
  await callbacks.onMessagePart(deepCopyPart(part));
@@ -49799,13 +50085,7 @@ function throwAbortError() {
49799
50085
  throw new DOMException("The operation was aborted.", "AbortError");
49800
50086
  }
49801
50087
  async function cancelStream(stream) {
49802
- const cancelable = stream;
49803
- try {
49804
- await cancelable.cancel?.();
49805
- } catch {}
49806
- try {
49807
- await cancelable.return?.();
49808
- } catch {}
50088
+ await stream[Symbol.asyncIterator]().return?.();
49809
50089
  }
49810
50090
  async function throwIfAborted(signal, stream) {
49811
50091
  if (!signal?.aborted) return;
@@ -49893,8 +50173,8 @@ function addUsage(a, b) {
49893
50173
  /**
49894
50174
  * Concrete provider adapters stay off the root barrel because their SDK type
49895
50175
  * graphs pollute downstream declaration bundles. Import them from subpaths:
49896
- * `@scream-cli/ltod/providers/scream`,
49897
- * `@scream-cli/ltod/providers/openai-legacy`, etc.
50176
+ * `@scream-code/ltod/providers/scream`,
50177
+ * `@scream-code/ltod/providers/openai-legacy`, etc.
49898
50178
  */
49899
50179
  //#endregion
49900
50180
  //#region ../../packages/agent-core/src/errors/serialize.ts
@@ -50727,7 +51007,7 @@ function resolveGlobalLogPath(homeDir) {
50727
51007
  * and supersedes this value. Used to keep `tokenCountWithPending`
50728
51008
  * monotonic between LLM round-trips without paying for a tokenizer.
50729
51009
  */
50730
- function estimateTokens(text) {
51010
+ function estimateTokens$1(text) {
50731
51011
  let asciiCount = 0;
50732
51012
  let nonAsciiCount = 0;
50733
51013
  for (const char of text) if (char.codePointAt(0) <= 127) asciiCount++;
@@ -50742,24 +51022,24 @@ function estimateTokensForMessages(messages) {
50742
51022
  function estimateTokensForTools(tools) {
50743
51023
  let total = 0;
50744
51024
  for (const tool of tools) {
50745
- total += estimateTokens(tool.name);
50746
- total += estimateTokens(tool.description);
50747
- total += estimateTokens(JSON.stringify(tool.parameters));
51025
+ total += estimateTokens$1(tool.name);
51026
+ total += estimateTokens$1(tool.description);
51027
+ total += estimateTokens$1(JSON.stringify(tool.parameters));
50748
51028
  }
50749
51029
  return total;
50750
51030
  }
50751
51031
  function estimateTokensForMessage(message) {
50752
- let total = estimateTokens(message.role);
51032
+ let total = estimateTokens$1(message.role);
50753
51033
  for (const part of message.content) total += estimateTokensForContentPart(part);
50754
51034
  if (message.toolCalls !== void 0) for (const call of message.toolCalls) {
50755
- total += estimateTokens(call.name);
50756
- total += estimateTokens(JSON.stringify(call.arguments));
51035
+ total += estimateTokens$1(call.name);
51036
+ total += estimateTokens$1(JSON.stringify(call.arguments));
50757
51037
  }
50758
51038
  return total;
50759
51039
  }
50760
51040
  function estimateTokensForContentPart(part) {
50761
- if (part.type === "text") return estimateTokens(part.text);
50762
- else if (part.type === "think") return estimateTokens(part.think);
51041
+ if (part.type === "text") return estimateTokens$1(part.text);
51042
+ else if (part.type === "think") return estimateTokens$1(part.think);
50763
51043
  return 0;
50764
51044
  }
50765
51045
  //#endregion
@@ -54586,7 +54866,7 @@ function fractionFromId(id) {
54586
54866
  if (Number.isFinite(n)) return n / 4294967296;
54587
54867
  }
54588
54868
  let hash = 5381;
54589
- for (let i = 0; i < id.length; i++) hash = (hash << 5) + hash + id.charCodeAt(i) | 0;
54869
+ for (let i = 0; i < id.length; i++) hash = (hash << 5) + hash + id.codePointAt(i) | 0;
54590
54870
  return (hash >>> 0) / 4294967296;
54591
54871
  }
54592
54872
  function jitterDisabledByEnv() {
@@ -54691,10 +54971,10 @@ var CronCreateTool = class {
54691
54971
  let parsed;
54692
54972
  try {
54693
54973
  parsed = parseCronExpression(normalizedCron);
54694
- } catch (err) {
54974
+ } catch (error) {
54695
54975
  return {
54696
54976
  isError: true,
54697
- output: `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`
54977
+ output: `Invalid cron expression: ${error instanceof Error ? error.message : String(error)}`
54698
54978
  };
54699
54979
  }
54700
54980
  const nowAtPrepare = this.manager.clocks.wallNow();
@@ -54711,7 +54991,7 @@ var CronCreateTool = class {
54711
54991
  isError: true,
54712
54992
  output: `Prompt exceeds ${String(MAX_PROMPT_BYTES)} bytes (got ${String(byteLen)}).`
54713
54993
  };
54714
- const recurring = args.recurring !== false;
54994
+ const recurring = args.recurring;
54715
54995
  if (!recurring) {
54716
54996
  const firstFire = computeNextCronRun(parsed, nowAtPrepare);
54717
54997
  if (firstFire !== null && firstFire - nowAtPrepare > ONE_SHOT_MAX_FUTURE_MS) return {
@@ -56518,7 +56798,7 @@ function createFastEmbedEngine() {
56518
56798
  if (texts.length === 0) return [];
56519
56799
  try {
56520
56800
  if (embedder === null) {
56521
- if (initPromise === null) initPromise = loadEmbedder();
56801
+ initPromise ??= loadEmbedder();
56522
56802
  embedder = await initPromise;
56523
56803
  if (embedder === null) {
56524
56804
  loadFailed = true;
@@ -56594,16 +56874,33 @@ var MemoryMemoStore = class MemoryMemoStore {
56594
56874
  * the types catch up, the synchronous calls here should be migrated to the
56595
56875
  * async API to avoid blocking the event loop on large operations.
56596
56876
  */
56877
+ initPromise = null;
56597
56878
  async init() {
56879
+ if (this.initPromise) return this.initPromise;
56880
+ this.initPromise = this._doInit();
56881
+ return this.initPromise;
56882
+ }
56883
+ async _doInit() {
56598
56884
  if (this.initialized) return;
56599
56885
  await this.ensureDir();
56600
56886
  this.db = new DatabaseSync(this.dbPath);
56601
56887
  this.db.exec("PRAGMA journal_mode = WAL;");
56602
56888
  this.db.exec("PRAGMA foreign_keys = ON;");
56889
+ this.db.exec("PRAGMA busy_timeout = 5000;");
56603
56890
  this.createSchema();
56604
56891
  await this.migrateFromJsonl();
56605
56892
  this.initialized = true;
56606
56893
  }
56894
+ /** Close the database handle and checkpoint WAL. */
56895
+ close() {
56896
+ if (this.db !== void 0) {
56897
+ this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
56898
+ this.db.close();
56899
+ this.db = void 0;
56900
+ }
56901
+ this.initialized = false;
56902
+ this.initPromise = null;
56903
+ }
56607
56904
  /** Iterate all memos from the database, newest first. Optionally filter by project directory. */
56608
56905
  async *read(options) {
56609
56906
  await this.init();
@@ -56709,9 +57006,7 @@ var MemoryMemoStore = class MemoryMemoStore {
56709
57006
  let stream;
56710
57007
  try {
56711
57008
  stream = createReadStream(legacyPath, { encoding: "utf8" });
56712
- } catch {
56713
- continue;
56714
- }
57009
+ } catch {}
56715
57010
  stream.on("error", () => {});
56716
57011
  let line = "";
56717
57012
  try {
@@ -56763,6 +57058,12 @@ var MemoryMemoStore = class MemoryMemoStore {
56763
57058
  projectDir: str(entry["projectDir"])
56764
57059
  };
56765
57060
  }
57061
+ const src = entry["extractionSource"];
57062
+ if (typeof src === "string" && ![
57063
+ "compaction",
57064
+ "exit",
57065
+ "manual"
57066
+ ].includes(src)) return;
56766
57067
  return entry;
56767
57068
  } catch {
56768
57069
  return;
@@ -56854,7 +57155,7 @@ var MemoryMemoStore = class MemoryMemoStore {
56854
57155
  }
56855
57156
  insertMany(memos) {
56856
57157
  if (this.db === void 0 || memos.length === 0) return;
56857
- const insert = this.db.prepare(`INSERT INTO memos (
57158
+ const insert = this.db.prepare(`INSERT OR IGNORE INTO memos (
56858
57159
  id, source_session_id, source_session_title, user_need, approach,
56859
57160
  outcome, what_failed, what_worked, extraction_source, recorded_at, project_dir, tags
56860
57161
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -56868,9 +57169,9 @@ var MemoryMemoStore = class MemoryMemoStore {
56868
57169
  insertFts.run(row.rowid, toFtsText(memo.userNeed), toFtsText(memo.approach), toFtsText(memo.whatFailed), toFtsText(memo.whatWorked), toFtsText(memo.sourceSessionTitle ?? ""));
56869
57170
  }
56870
57171
  this.db.exec("COMMIT");
56871
- } catch (err) {
57172
+ } catch (error) {
56872
57173
  this.db.exec("ROLLBACK");
56873
- throw new Error(`Failed to migrate memos to SQLite: ${err instanceof Error ? err.message : String(err)}`);
57174
+ throw new Error(`Failed to migrate memos to SQLite: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
56874
57175
  }
56875
57176
  }
56876
57177
  async appendInternal(entry) {
@@ -56983,6 +57284,10 @@ var MemoryMemoStore = class MemoryMemoStore {
56983
57284
  * Search memos by vector similarity. Returns memos sorted by cosine
56984
57285
  * similarity (highest first). Falls back to empty if no embeddings exist.
56985
57286
  *
57287
+ * NOTE: candidates are ordered by created_at DESC, so only the newest
57288
+ * N embeddings are considered. Older but highly relevant memos may be
57289
+ * omitted when the store has more than candidateLimit embeddings.
57290
+ * This is a known limitation — see M-M5 in project review notes.
56986
57291
  * Performance notes:
56987
57292
  * - candidateLimit bounds the SQL query so we never load every embedding.
56988
57293
  * - recencyCutoffDays lets callers ignore very old memos.
@@ -57088,12 +57393,12 @@ var MemoryMemoStore = class MemoryMemoStore {
57088
57393
  try {
57089
57394
  for (let i = 0; i < pending.length; i++) insert.run(pending[i].id, JSON.stringify([...vectors[i]]), "bge-small-zh-v1.5", now);
57090
57395
  this.db.exec("COMMIT");
57091
- } catch (err) {
57396
+ } catch (error) {
57092
57397
  this.db.exec("ROLLBACK");
57093
- this.markEmbeddingFailure(err instanceof Error ? err : new Error(String(err)));
57398
+ this.markEmbeddingFailure(error instanceof Error ? error : new Error(String(error)));
57094
57399
  }
57095
- } catch (err) {
57096
- this.markEmbeddingFailure(err instanceof Error ? err : new Error(String(err)));
57400
+ } catch (error) {
57401
+ this.markEmbeddingFailure(error instanceof Error ? error : new Error(String(error)));
57097
57402
  } finally {
57098
57403
  this.embeddingFlushing = false;
57099
57404
  }
@@ -57195,7 +57500,7 @@ function memoMatchesSearch(memo, search) {
57195
57500
  memo.whatWorked,
57196
57501
  memo.sourceSessionTitle ?? "",
57197
57502
  ...memo.tags ?? []
57198
- ].join(" ").toLowerCase().includes(search);
57503
+ ].join(" ").toLowerCase().replaceAll(/\s+/g, "").includes(search.replaceAll(/\s+/g, ""));
57199
57504
  }
57200
57505
  /**
57201
57506
  * Tokenize text so FTS5's unicode61 tokenizer can index mixed CJK/ASCII text.
@@ -57336,13 +57641,41 @@ async function buildConsolidationPlan(store, options) {
57336
57641
  async function applyConsolidation(store, plan) {
57337
57642
  let deleted = 0;
57338
57643
  let created = 0;
57339
- for (const memo of plan.resolved) {
57340
- await store.delete(memo.id);
57341
- deleted++;
57342
- }
57343
- for (const memo of plan.stale) {
57344
- await store.delete(memo.id);
57345
- deleted++;
57644
+ if (plan.resolved.length > 0) {
57645
+ const worked = plan.resolved.filter((m) => m.whatWorked).map((m) => m.whatWorked).join("; ");
57646
+ const failed = plan.resolved.filter((m) => m.whatFailed).map((m) => m.whatFailed).join("; ");
57647
+ if (worked || failed) {
57648
+ const archive = createMemoryMemo({
57649
+ sourceSessionId: plan.resolved[0].sourceSessionId,
57650
+ userNeed: "已解决任务的经验归档(dream 整理)",
57651
+ approach: "多次尝试后解决",
57652
+ outcome: "已完成",
57653
+ whatWorked: worked || "none",
57654
+ whatFailed: failed || "none",
57655
+ tags: normalizeTags(plan.resolved.flatMap((m) => m.tags ?? [])),
57656
+ extractionSource: "compaction"
57657
+ });
57658
+ await store.append(archive);
57659
+ created++;
57660
+ }
57661
+ }
57662
+ if (plan.stale.length > 0) {
57663
+ const worked = plan.stale.filter((m) => m.whatWorked).map((m) => m.whatWorked).join("; ");
57664
+ const failed = plan.stale.filter((m) => m.whatFailed).map((m) => m.whatFailed).join("; ");
57665
+ if (worked || failed) {
57666
+ const archive = createMemoryMemo({
57667
+ sourceSessionId: plan.stale[0].sourceSessionId,
57668
+ userNeed: "过期记忆的经验归档(dream 整理)",
57669
+ approach: "距今超过30天未使用",
57670
+ outcome: "已归档",
57671
+ whatWorked: worked || "none",
57672
+ whatFailed: failed || "none",
57673
+ tags: normalizeTags(plan.stale.flatMap((m) => m.tags ?? [])),
57674
+ extractionSource: "compaction"
57675
+ });
57676
+ await store.append(archive);
57677
+ created++;
57678
+ }
57346
57679
  }
57347
57680
  for (const group of plan.duplicateGroups) {
57348
57681
  const newest = group.memos.reduce((a, b) => a.recordedAt > b.recordedAt ? a : b);
@@ -57358,13 +57691,21 @@ async function applyConsolidation(store, plan) {
57358
57691
  tags: group.merged.tags ?? mergedTags,
57359
57692
  extractionSource: "compaction"
57360
57693
  });
57361
- for (const memo of group.memos) {
57362
- await store.delete(memo.id);
57363
- deleted++;
57364
- }
57365
57694
  await store.append(merged);
57366
57695
  created++;
57367
57696
  }
57697
+ for (const group of plan.duplicateGroups) for (const memo of group.memos) {
57698
+ await store.delete(memo.id);
57699
+ deleted++;
57700
+ }
57701
+ for (const memo of plan.resolved) {
57702
+ await store.delete(memo.id);
57703
+ deleted++;
57704
+ }
57705
+ for (const memo of plan.stale) {
57706
+ await store.delete(memo.id);
57707
+ deleted++;
57708
+ }
57368
57709
  return {
57369
57710
  deleted,
57370
57711
  created
@@ -57381,7 +57722,7 @@ function findDuplicateGroups(memos) {
57381
57722
  const candidate = memos[j];
57382
57723
  if (!candidate || used.has(candidate.id)) continue;
57383
57724
  if (cluster.some((m) => {
57384
- return computeRelevanceScore(candidate, `${m.userNeed} ${m.approach}`) >= SIMILARITY_THRESHOLD;
57725
+ return computeKeywordSimilarity(candidate, `${m.userNeed} ${m.approach}`) >= SIMILARITY_THRESHOLD;
57385
57726
  })) cluster.push(candidate);
57386
57727
  }
57387
57728
  if (cluster.length > 1) {
@@ -57967,7 +58308,7 @@ var MemoryWriteTool = class {
57967
58308
  const sourceSessionTitle = await this.agent.getSessionTitle();
57968
58309
  const whatFailed = args.whatFailed?.trim();
57969
58310
  const whatWorked = args.whatWorked?.trim();
57970
- const tags = args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${args.userNeed} ${args.approach}`);
58311
+ const tags = normalizeTags(args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${args.userNeed} ${args.approach}`));
57971
58312
  const memo = createMemoryMemo({
57972
58313
  sourceSessionId: sessionId,
57973
58314
  sourceSessionTitle,
@@ -58032,7 +58373,14 @@ var LspClient = class {
58032
58373
  await this.request("initialize", {
58033
58374
  processId: process.pid,
58034
58375
  rootUri: pathToUri(this.workspaceRoot),
58035
- capabilities: {}
58376
+ capabilities: { textDocument: {
58377
+ synchronization: {
58378
+ willSave: false,
58379
+ willSaveWaitUntil: false,
58380
+ didSave: false
58381
+ },
58382
+ rename: { prepareSupport: false }
58383
+ } }
58036
58384
  });
58037
58385
  this.notify("initialized", {});
58038
58386
  }
@@ -58079,6 +58427,16 @@ var LspClient = class {
58079
58427
  if (result === null) return [];
58080
58428
  return Array.isArray(result) ? result : [result];
58081
58429
  }
58430
+ async rename(path, line, character, newName) {
58431
+ return await this.request("textDocument/rename", {
58432
+ textDocument: { uri: pathToUri(path) },
58433
+ position: {
58434
+ line,
58435
+ character
58436
+ },
58437
+ newName
58438
+ });
58439
+ }
58082
58440
  async diagnostics(path, timeoutMs = 5e3) {
58083
58441
  const uri = pathToUri(path);
58084
58442
  this.collectedDiagnostics.delete(uri);
@@ -58174,6 +58532,19 @@ function pathToUri(path) {
58174
58532
  }
58175
58533
  return `file://${path.startsWith("/") ? path : `/${path}`}`;
58176
58534
  }
58535
+ /**
58536
+ * Convert a `file://` URI back to a filesystem path. Tolerates
58537
+ * percent-encoded characters and lax servers that send raw paths.
58538
+ */
58539
+ function uriToPath(uri) {
58540
+ if (!uri.startsWith("file://")) return uri;
58541
+ let filePath = uri.slice(7);
58542
+ try {
58543
+ filePath = decodeURIComponent(filePath);
58544
+ } catch {}
58545
+ if (process.platform === "win32" && filePath.startsWith("/") && /^[A-Za-z]:/.test(filePath.slice(1))) filePath = filePath.slice(1);
58546
+ return filePath;
58547
+ }
58177
58548
  function sleep$1(ms) {
58178
58549
  return new Promise((resolve) => {
58179
58550
  setTimeout(resolve, ms);
@@ -58189,24 +58560,141 @@ function formatDiagnostic(diagnostic) {
58189
58560
  return `- ${diagnostic.severity !== void 0 ? SEVERITY_LABELS[diagnostic.severity - 1] ?? "Unknown" : "Diagnostic"} at ${start.line + 1}:${start.character + 1}: ${diagnostic.message}`;
58190
58561
  }
58191
58562
  //#endregion
58563
+ //#region ../../packages/agent-core/src/lsp/edits.ts
58564
+ function comparePosition(a, b) {
58565
+ return a.line === b.line ? a.character - b.character : a.line - b.line;
58566
+ }
58567
+ /**
58568
+ * Sort edits bottom-to-top for in-place application and reject overlaps.
58569
+ * Equal start positions tiebreak by original array index descending so that,
58570
+ * applied bottom-up, inserts at the same position land in array order
58571
+ * (LSP spec: the order of edits in the array defines the order in the result).
58572
+ */
58573
+ function sortAndValidateTextEdits(edits) {
58574
+ const sorted = edits.map((edit, index) => ({
58575
+ edit,
58576
+ index
58577
+ })).sort((a, b) => {
58578
+ const sa = a.edit.range.start;
58579
+ const sb = b.edit.range.start;
58580
+ if (sa.line !== sb.line) return sb.line - sa.line;
58581
+ if (sa.character !== sb.character) return sb.character - sa.character;
58582
+ return b.index - a.index;
58583
+ }).map((entry) => entry.edit);
58584
+ for (let i = 0; i < sorted.length - 1; i++) {
58585
+ const later = sorted[i].range;
58586
+ const earlier = sorted[i + 1].range;
58587
+ if (comparePosition(earlier.end, later.start) > 0) throw new Error(`overlapping LSP edits at ${earlier.start.line + 1}:${earlier.start.character + 1} conflict with ${later.start.line + 1}:${later.start.character + 1}; multi-server rename produced inconsistent edits`);
58588
+ }
58589
+ return sorted;
58590
+ }
58591
+ /**
58592
+ * Apply text edits to a string in-memory. Edits are applied in reverse
58593
+ * order (bottom-to-top) to preserve line/character indices.
58594
+ */
58595
+ function applyTextEditsToString(content, edits) {
58596
+ const lines = content.split("\n");
58597
+ const sortedEdits = sortAndValidateTextEdits(edits);
58598
+ for (const edit of sortedEdits) {
58599
+ const { start, end } = edit.range;
58600
+ if (start.line === end.line) {
58601
+ const line = lines[start.line] ?? "";
58602
+ lines[start.line] = line.slice(0, start.character) + edit.newText + line.slice(end.character);
58603
+ } else {
58604
+ const startLine = lines[start.line] ?? "";
58605
+ const endLine = lines[end.line] ?? "";
58606
+ const newContent = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
58607
+ lines.splice(start.line, end.line - start.line + 1, ...newContent.split("\n"));
58608
+ }
58609
+ }
58610
+ return lines.join("\n");
58611
+ }
58612
+ /**
58613
+ * Flatten a WorkspaceEdit's text edits into a Map<path, TextEdit[]>.
58614
+ * Resource operations (create/rename/delete) are ignored — callers handle
58615
+ * them separately.
58616
+ */
58617
+ function flattenWorkspaceTextEdits(edit) {
58618
+ const out = /* @__PURE__ */ new Map();
58619
+ const push = (uri, edits) => {
58620
+ if (edits.length === 0) return;
58621
+ const path = uriToPath(uri);
58622
+ const prev = out.get(path);
58623
+ if (prev) prev.push(...edits);
58624
+ else out.set(path, [...edits]);
58625
+ };
58626
+ if (edit.changes) for (const uri of Object.keys(edit.changes)) push(uri, edit.changes[uri]);
58627
+ if (edit.documentChanges) {
58628
+ for (const change of edit.documentChanges) if (change && change.textDocument && change.edits) push(change.textDocument.uri, change.edits);
58629
+ }
58630
+ return out;
58631
+ }
58632
+ /**
58633
+ * Apply a workspace edit (collection of file changes) to disk via Jian.
58634
+ *
58635
+ * All text-edit batches are overlap-validated before anything is written
58636
+ * so a conflict throws without leaving the workspace half-applied. Every
58637
+ * target path is passed through `validatePath` first — throws
58638
+ * `LspEditPathValidationError` if any path is outside the allowed root.
58639
+ */
58640
+ async function applyWorkspaceEdit(edit, jian, validatePath) {
58641
+ const flattened = flattenWorkspaceTextEdits(edit);
58642
+ for (const edits of flattened.values()) sortAndValidateTextEdits(edits);
58643
+ if (validatePath !== void 0) for (const filePath of flattened.keys()) validatePath(filePath);
58644
+ const applied = [];
58645
+ for (const [filePath, edits] of flattened) {
58646
+ const result = applyTextEditsToString(await jian.readText(filePath), edits);
58647
+ await jian.writeText(filePath, result);
58648
+ applied.push({
58649
+ filePath,
58650
+ editCount: edits.length
58651
+ });
58652
+ }
58653
+ return applied;
58654
+ }
58655
+ /**
58656
+ * Format a workspace edit as a summary list (one entry per affected file).
58657
+ * Used for the preview mode (`apply: false`) where we show what *would*
58658
+ * change without writing to disk.
58659
+ */
58660
+ function formatWorkspaceEditPreview(edit) {
58661
+ const lines = [];
58662
+ if (edit.changes) for (const uri of Object.keys(edit.changes)) {
58663
+ const edits = edit.changes[uri];
58664
+ const path = uriToPath(uri);
58665
+ lines.push(`${path}: ${String(edits.length)} edit${edits.length > 1 ? "s" : ""}`);
58666
+ }
58667
+ if (edit.documentChanges) {
58668
+ for (const change of edit.documentChanges) if (change && change.textDocument && change.edits) {
58669
+ const path = uriToPath(change.textDocument.uri);
58670
+ lines.push(`${path}: ${String(change.edits.length)} edit${change.edits.length > 1 ? "s" : ""}`);
58671
+ }
58672
+ }
58673
+ return lines;
58674
+ }
58675
+ //#endregion
58192
58676
  //#region ../../packages/agent-core/src/tools/builtin/lsp-tool.ts
58193
58677
  const LspInputSchema = z.object({
58194
58678
  path: z.string().describe("Path to the source file. Relative paths resolve against the working directory; a path outside the working directory must be absolute."),
58195
58679
  operation: z.enum([
58196
58680
  "references",
58197
58681
  "definition",
58198
- "diagnostics"
58199
- ]).describe("LSP operation to perform: 'references', 'definition', or 'diagnostics'."),
58200
- line: z.number().int().min(1).optional().describe("1-based line number for references/definition."),
58201
- character: z.number().int().min(0).optional().describe("0-based column/character offset for references/definition."),
58202
- include_declaration: z.boolean().optional().describe("For 'references': whether to include the declaration site in the results.")
58682
+ "diagnostics",
58683
+ "rename"
58684
+ ]).describe("LSP operation to perform: 'references', 'definition', 'diagnostics', or 'rename'."),
58685
+ line: z.number().int().min(1).optional().describe("1-based line number for references/definition/rename."),
58686
+ character: z.number().int().min(0).optional().describe("0-based column/character offset for references/definition/rename."),
58687
+ include_declaration: z.boolean().optional().describe("For 'references': whether to include the declaration site in the results."),
58688
+ new_name: z.string().optional().describe("For 'rename': the new symbol name. Required for rename. The rename is applied to disk unless `apply` is false."),
58689
+ apply: z.boolean().optional().describe("For 'rename': when true, apply the rename to disk across all affected files. When false (default), return a preview of the changes without writing. Pass true only after previewing.")
58203
58690
  });
58204
58691
  /**
58205
- * LSP tool — read-only code intelligence via language servers.
58692
+ * LSP tool — code intelligence via language servers.
58206
58693
  *
58207
- * Supports references, go-to-definition, and diagnostics. The file content is
58208
- * opened in the language server so results reflect the current editor state
58209
- * even when the file has not been saved to disk.
58694
+ * Supports references, go-to-definition, diagnostics, and rename. The file
58695
+ * content is opened in the language server so results reflect the current
58696
+ * editor state even when the file has not been saved to disk. Rename is the
58697
+ * only write op; pass `apply: false` to preview without writing.
58210
58698
  */
58211
58699
  var LspTool = class {
58212
58700
  agent;
@@ -58214,10 +58702,10 @@ var LspTool = class {
58214
58702
  lspRegistry;
58215
58703
  name = "LSP";
58216
58704
  description = [
58217
- "Query a language server for read-only code intelligence.",
58218
- "Use 'references' to find all usages of a symbol, 'definition' to jump to where a symbol is defined, and 'diagnostics' to get type errors and warnings for a file.",
58705
+ "Query a language server for code intelligence.",
58706
+ "Use 'references' to find all usages of a symbol, 'definition' to jump to where a symbol is defined, 'diagnostics' to get type errors and warnings for a file, and 'rename' to rename a symbol across all its references.",
58219
58707
  "The language server is started automatically for supported file types (TypeScript/JavaScript, Python, Rust, Go).",
58220
- "This tool does not edit files; use the results to inform Read/Edit operations."
58708
+ "Rename requires the typescript-language-server (or equivalent) binary on PATH for the file type."
58221
58709
  ].join(" ");
58222
58710
  parameters = toInputJsonSchema(LspInputSchema);
58223
58711
  constructor(agent, workspace, lspRegistry) {
@@ -58226,13 +58714,14 @@ var LspTool = class {
58226
58714
  this.lspRegistry = lspRegistry;
58227
58715
  }
58228
58716
  resolveExecution(args) {
58717
+ const isWrite = args.operation === "rename" && args.apply === true;
58229
58718
  const path = resolvePathAccessPath(args.path, {
58230
58719
  jian: this.agent.jian,
58231
58720
  workspace: this.workspace,
58232
- operation: "read"
58721
+ operation: isWrite ? "write" : "read"
58233
58722
  });
58234
58723
  return {
58235
- accesses: ToolAccesses.readFile(path),
58724
+ accesses: isWrite ? ToolAccesses.writeFile(path) : ToolAccesses.readFile(path),
58236
58725
  description: `LSP ${args.operation} ${args.path}`,
58237
58726
  approvalRule: this.name,
58238
58727
  execute: () => this.execution(args, path)
@@ -58315,6 +58804,44 @@ var LspTool = class {
58315
58804
  ].join("\n")
58316
58805
  };
58317
58806
  }
58807
+ case "rename": {
58808
+ if (args.line === void 0 || args.character === void 0) return {
58809
+ isError: true,
58810
+ output: "'rename' requires both 'line' and 'character'."
58811
+ };
58812
+ if (args.new_name === void 0 || args.new_name === "") return {
58813
+ isError: true,
58814
+ output: "'rename' requires 'new_name'."
58815
+ };
58816
+ const workspaceEdit = await client.rename(safePath, args.line - 1, args.character, args.new_name);
58817
+ if (workspaceEdit === null) return {
58818
+ isError: false,
58819
+ output: "Rename returned no edits."
58820
+ };
58821
+ if (args.apply === true) {
58822
+ const applied = await applyWorkspaceEdit(workspaceEdit, this.agent.jian, (p) => {
58823
+ if (!isWithinWorkspace(p, this.workspace, this.agent.jian.pathClass())) throw new Error(`Refusing to apply rename: LSP returned edits for a file outside the workspace: ${p}`);
58824
+ });
58825
+ if (applied.length === 0) return {
58826
+ isError: false,
58827
+ output: "Rename produced no file changes."
58828
+ };
58829
+ const lines = applied.map((a) => ` Applied ${String(a.editCount)} edit(s) to ${a.filePath}`);
58830
+ return {
58831
+ isError: false,
58832
+ output: [`Applied rename to ${String(applied.length)} file(s):`, ...lines].join("\n")
58833
+ };
58834
+ }
58835
+ const preview = formatWorkspaceEditPreview(workspaceEdit);
58836
+ if (preview.length === 0) return {
58837
+ isError: false,
58838
+ output: "Rename preview is empty."
58839
+ };
58840
+ return {
58841
+ isError: false,
58842
+ output: [`Rename preview (${String(preview.length)} file(s)):`, ...preview].join("\n")
58843
+ };
58844
+ }
58318
58845
  default: return {
58319
58846
  isError: true,
58320
58847
  output: `Unsupported operation: ${String(args.operation)}`
@@ -68759,7 +69286,9 @@ async function resolveGithubSource(input) {
68759
69286
  }
68760
69287
  };
68761
69288
  const headController = new AbortController();
68762
- const headTimeout = setTimeout(() => headController.abort(), 3e4);
69289
+ const headTimeout = setTimeout(() => {
69290
+ headController.abort();
69291
+ }, 3e4);
68763
69292
  let headProbe;
68764
69293
  try {
68765
69294
  headProbe = await fetch(`https://codeload.github.com/${owner}/${repo}/zip/HEAD`, {
@@ -68795,7 +69324,9 @@ async function resolveGithubSource(input) {
68795
69324
  async function tryResolveLatestReleaseTag(owner, repo) {
68796
69325
  const url = `https://github.com/${owner}/${repo}/releases/latest`;
68797
69326
  const controller = new AbortController();
68798
- const timeoutHandle = setTimeout(() => controller.abort(), 3e4);
69327
+ const timeoutHandle = setTimeout(() => {
69328
+ controller.abort();
69329
+ }, 3e4);
68799
69330
  let resp;
68800
69331
  try {
68801
69332
  resp = await fetch(url, {
@@ -69487,7 +70018,7 @@ async function discoverSkillDirs(root, maxDepth) {
69487
70018
  }
69488
70019
  }
69489
70020
  await walk(root, 1);
69490
- const sorted = found.sort((a, b) => a.length - b.length);
70021
+ const sorted = found.toSorted((a, b) => a.length - b.length);
69491
70022
  const result = [];
69492
70023
  for (const dir of sorted) if (!result.some((parent) => dir.startsWith(parent + path$1.sep))) result.push(dir);
69493
70024
  return result;
@@ -70442,7 +70973,7 @@ var wolfpack_default = "Use WolfPack to spawn multiple subagents in parallel for
70442
70973
  *
70443
70974
  * Spawns multiple subagents in parallel using a template + items pattern.
70444
70975
  * Each item gets its own subagent; results are batched together.
70445
- * V1 uses Promise.allSettled no concurrency control or rate-limit handling.
70976
+ * Concurrency capped at WOLFPACK_CONCURRENCY to avoid provider rate-limit exhaustion.
70446
70977
  */
70447
70978
  const MAX_ITEMS = 20;
70448
70979
  const WolfPackToolInputSchema = z.object({
@@ -70489,27 +71020,51 @@ var WolfPackTool = class {
70489
71020
  };
70490
71021
  const profileName = args.subagent_type ?? "coder";
70491
71022
  const template = args.prompt_template;
71023
+ const WOLFPACK_CONCURRENCY = 8;
70492
71024
  const handlePromises = args.items.map(async (item) => {
70493
71025
  ctx.signal.throwIfAborted();
70494
- const prompt = template.replace(/\{\{item\}\}/g, item);
70495
- return {
70496
- item,
70497
- handle: await this.subagentHost.spawn(profileName, {
70498
- parentToolCallId: ctx.toolCallId,
70499
- prompt,
70500
- description: `${args.description}: ${item}`,
70501
- runInBackground: false,
70502
- signal: ctx.signal
70503
- })
70504
- };
71026
+ try {
71027
+ const prompt = template.replaceAll("{{item}}", () => item);
71028
+ return {
71029
+ item,
71030
+ handle: await this.subagentHost.spawn(profileName, {
71031
+ parentToolCallId: ctx.toolCallId,
71032
+ prompt,
71033
+ description: `${args.description}: ${item}`,
71034
+ runInBackground: false,
71035
+ signal: ctx.signal
71036
+ })
71037
+ };
71038
+ } catch (error) {
71039
+ return {
71040
+ item,
71041
+ error
71042
+ };
71043
+ }
70505
71044
  });
70506
- const completionPromises = (await Promise.allSettled(handlePromises)).map(async (settled) => {
71045
+ const handleResults = [];
71046
+ for (let i = 0; i < handlePromises.length; i += WOLFPACK_CONCURRENCY) {
71047
+ ctx.signal.throwIfAborted();
71048
+ const batch = handlePromises.slice(i, i + WOLFPACK_CONCURRENCY);
71049
+ const batchResults = await Promise.allSettled(batch);
71050
+ handleResults.push(...batchResults);
71051
+ }
71052
+ const completionPromises = handleResults.map(async (settled) => {
70507
71053
  if (settled.status === "rejected") return {
70508
71054
  item: "unknown",
70509
71055
  result: `Spawn failed: ${settled.reason instanceof Error ? settled.reason.message : String(settled.reason)}`,
70510
71056
  success: false
70511
71057
  };
70512
- const { item, handle } = settled.value;
71058
+ const value = settled.value;
71059
+ if ("error" in value) {
71060
+ const msg = value.error instanceof Error ? value.error.message : String(value.error);
71061
+ return {
71062
+ item: value.item,
71063
+ result: `Spawn failed: ${msg}`,
71064
+ success: false
71065
+ };
71066
+ }
71067
+ const { item, handle } = value;
70513
71068
  try {
70514
71069
  return {
70515
71070
  item,
@@ -75655,7 +76210,7 @@ var ReadGroupTool = class {
75655
76210
  });
75656
76211
  }
75657
76212
  const accesses = executions.filter((e) => "exec" in e).flatMap((e) => e.exec.accesses ?? ToolAccesses.none());
75658
- const sortedPaths = [...paths].sort();
76213
+ const sortedPaths = [...paths].toSorted();
75659
76214
  const approvalRule = literalRulePattern(this.name, sortedPaths.join("\n"));
75660
76215
  const deniedCount = executions.filter((e) => "error" in e).length;
75661
76216
  return {
@@ -76267,7 +76822,7 @@ function commandNotFoundHint(command) {
76267
76822
  return "Hint: The command binary was not found. Check that the required toolchain is installed and use the project-specific script when available.";
76268
76823
  }
76269
76824
  function validateCommand(command, isWindows) {
76270
- const cmd = command;
76825
+ const cmd = command.replace(/^(?:command\s+|builtin\s+)+/, "");
76271
76826
  if (/\bkill\s+-9\s+-1\b/.test(cmd) || /\bkill\s+-KILL\s+-1\b/.test(cmd)) return rejectDangerousCommand("kill -9 -1", "Use 'kill <pid>' with a specific PID to terminate the target process.");
76272
76827
  if (/\b(killall|pkill)\b.*\b(node|scream)/i.test(cmd)) return rejectDangerousCommand("killall/pkill node", "Use 'kill <pid>' with a specific PID, or use the server's own stop command.");
76273
76828
  if (isWindows) {
@@ -76555,7 +77110,7 @@ const TodoItemSchema = z.object({
76555
77110
  ]).describe("Current status of the todo."),
76556
77111
  phase: z.string().optional().describe("Optional phase/group for the todo. Items in the same phase are rendered together. Complete one phase before starting the next.")
76557
77112
  });
76558
- const TodoListInputSchema = z.object({ todos: z.array(TodoItemSchema).optional().describe("The updated todo list. Omit to read the current todo list without making changes. Pass an empty array to clear the list.") });
77113
+ const TodoListInputSchema = z.object({ todos: z.array(TodoItemSchema).max(200).optional().describe("The updated todo list. Omit to read the current todo list without making changes. Pass an empty array to clear the list.") });
76559
77114
  function renderTodoList(todos) {
76560
77115
  if (todos.length === 0) return "Todo list is empty.";
76561
77116
  const groups = /* @__PURE__ */ new Map();
@@ -77070,11 +77625,15 @@ async function chatWithRetry(input) {
77070
77625
  logRequestFailure(input, error, attempt, maxAttempts);
77071
77626
  throw error;
77072
77627
  }
77628
+ if (error instanceof APIProviderRateLimitError && error.reason === "QUOTA_EXHAUSTED") {
77629
+ logRequestFailure(input, error, attempt, maxAttempts);
77630
+ throw error;
77631
+ }
77073
77632
  if (attempt >= maxAttempts || !input.llm.isRetryableError(error)) {
77074
77633
  logRequestFailure(input, error, attempt, maxAttempts);
77075
77634
  throw error;
77076
77635
  }
77077
- const delayMs = delays[attempt - 1] ?? 0;
77636
+ const delayMs = computeDelayMs(error, delays, attempt);
77078
77637
  input.params.signal.throwIfAborted();
77079
77638
  input.dispatchEvent({
77080
77639
  type: "step.retrying",
@@ -77090,6 +77649,10 @@ async function chatWithRetry(input) {
77090
77649
  await sleepForRetry(delayMs, input.params.signal);
77091
77650
  }
77092
77651
  }
77652
+ function computeDelayMs(error, delays, attempt) {
77653
+ if (error instanceof APIProviderRateLimitError) return calculateRateLimitBackoffMs(error.reason);
77654
+ return delays[attempt - 1] ?? 0;
77655
+ }
77093
77656
  function logRequestFailure(input, error, attempt, maxAttempts) {
77094
77657
  if (isAbortError$1(error) || input.params.signal.aborted) return;
77095
77658
  input.log?.warn("llm request failed", {
@@ -77334,6 +77897,7 @@ var DefaultCompactionStrategy = class {
77334
77897
  }
77335
77898
  shouldCompactProactively(usedSize, maxOutputTokens) {
77336
77899
  if (this.maxSize <= 0) return false;
77900
+ if (this.config.triggerRatio >= 1) return false;
77337
77901
  return usedSize + this.estimateTurnGrowth(maxOutputTokens) >= this.maxSize;
77338
77902
  }
77339
77903
  get checkAfterStep() {
@@ -77377,6 +77941,8 @@ var FullCompaction = class {
77377
77941
  agent;
77378
77942
  compactionCountInTurn = 0;
77379
77943
  consecutiveCompactionFailures = 0;
77944
+ _shouldInjectSessionSummary = false;
77945
+ compactionTimedOut = false;
77380
77946
  compacting = null;
77381
77947
  _compactedHistory = [];
77382
77948
  strategy;
@@ -77394,6 +77960,14 @@ var FullCompaction = class {
77394
77960
  get compactedHistory() {
77395
77961
  return this._compactedHistory;
77396
77962
  }
77963
+ /** One-shot: true if session memory summary should be injected at the next step. */
77964
+ shouldInjectSessionSummary() {
77965
+ if (this._shouldInjectSessionSummary) {
77966
+ this._shouldInjectSessionSummary = false;
77967
+ return true;
77968
+ }
77969
+ return false;
77970
+ }
77397
77971
  begin(data) {
77398
77972
  if (this.compacting) return;
77399
77973
  if (data.source === "manual") this.compactionCountInTurn = 0;
@@ -77449,13 +78023,22 @@ var FullCompaction = class {
77449
78023
  this.consecutiveCompactionFailures = 0;
77450
78024
  }
77451
78025
  async handleOverflowError(signal, error) {
77452
- if (!this.beginAutoCompaction() && !this.compacting) throw error;
77453
- await this.block(signal);
78026
+ if (!this.beginAutoCompaction(false) && !this.compacting) {
78027
+ if (this.consecutiveCompactionFailures >= MAX_CONSECUTIVE_FAILURES) this.agent.emitEvent({
78028
+ type: "warning",
78029
+ message: "压缩熔断器已打开,使用 /compact 手动重试。",
78030
+ code: "compaction_circuit_open_overflow"
78031
+ });
78032
+ throw error;
78033
+ }
77454
78034
  }
77455
78035
  async beforeStep(signal) {
77456
78036
  this.agent.microCompaction.detect();
77457
78037
  const effectiveTokens = this.effectiveTokenCount;
77458
- if (this.strategy.shouldCompact(effectiveTokens) || this.strategy.shouldCompactProactively(effectiveTokens, this.estimatedMaxOutputTokens)) this.checkAutoCompaction();
78038
+ const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens);
78039
+ const isProactiveTrigger = !isReactiveTrigger && this.strategy.shouldCompactProactively(effectiveTokens, this.estimatedMaxOutputTokens);
78040
+ if (isReactiveTrigger) this.checkAutoCompaction();
78041
+ else if (isProactiveTrigger) this.beginAutoCompaction();
77459
78042
  if (this.strategy.shouldBlock(effectiveTokens)) await this.block(signal);
77460
78043
  }
77461
78044
  /** Conservative estimate of max output tokens for one API call. */
@@ -77497,7 +78080,10 @@ var FullCompaction = class {
77497
78080
  if (!active) return;
77498
78081
  active.blockedByTurn = true;
77499
78082
  const timeoutId = setTimeout(() => {
77500
- if (this.compacting === active) this.markCanceled("压缩超时(60秒),已取消。请使用 /compact 手动重试。");
78083
+ if (this.compacting === active) {
78084
+ this.compactionTimedOut = true;
78085
+ this.markCanceled("压缩超时(60秒),已取消。请使用 /compact 手动重试。");
78086
+ }
77501
78087
  }, 6e4);
77502
78088
  const onAbort = () => {
77503
78089
  clearTimeout(timeoutId);
@@ -77558,7 +78144,7 @@ var FullCompaction = class {
77558
78144
  return;
77559
78145
  }
77560
78146
  const recent = originalHistory.slice(compactedCount);
77561
- const tokensAfter = estimateTokens(summary) + estimateTokensForMessages(recent);
78147
+ const tokensAfter = estimateTokens$1(summary) + estimateTokensForMessages(recent);
77562
78148
  const result = {
77563
78149
  summary,
77564
78150
  compactedCount,
@@ -77574,8 +78160,11 @@ var FullCompaction = class {
77574
78160
  await this.extractAndStoreMemos(summary);
77575
78161
  this.triggerPostCompactHook(data, result);
77576
78162
  this.consecutiveCompactionFailures = 0;
78163
+ this._shouldInjectSessionSummary = true;
77577
78164
  } catch (error) {
77578
- if (!isAbortError$1(error)) {
78165
+ const wasTimedOut = this.compactionTimedOut;
78166
+ this.compactionTimedOut = false;
78167
+ if (!isAbortError$1(error) || wasTimedOut) {
77579
78168
  const blockedByTurn = this.compacting?.blockedByTurn === true;
77580
78169
  this.consecutiveCompactionFailures += 1;
77581
78170
  if (this.consecutiveCompactionFailures >= MAX_CONSECUTIVE_FAILURES) this.agent.emitEvent({
@@ -77751,7 +78340,7 @@ var MicroCompaction = class {
77751
78340
  const { history } = this.agent.context;
77752
78341
  const maxContextTokens = this.agent.config.modelCapabilities.max_context_tokens;
77753
78342
  const contextTokens = this.agent.context.tokenCountWithPending;
77754
- if ((maxContextTokens !== void 0 && maxContextTokens > 0 ? contextTokens / maxContextTokens : 1) < config.minContextUsageRatio) return;
78343
+ if ((maxContextTokens !== void 0 && maxContextTokens > 0 ? contextTokens / maxContextTokens : 0) < config.minContextUsageRatio) return;
77755
78344
  const nextCutoff = Math.max(0, history.length - config.keepRecentMessages);
77756
78345
  this.apply(nextCutoff);
77757
78346
  }
@@ -77800,7 +78389,7 @@ var MicroCompaction = class {
77800
78389
  if (message?.role !== "tool" || message.toolCallId === void 0) continue;
77801
78390
  const contentTokens = estimateTokensForMessages([message]);
77802
78391
  if (contentTokens < this.config.minContentTokens) continue;
77803
- markerTokenCount ??= estimateTokens(this.config.truncatedMarker);
78392
+ markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
77804
78393
  truncatedToolResultCount += 1;
77805
78394
  beforeTokens += contentTokens;
77806
78395
  afterTokens += markerTokenCount;
@@ -78275,7 +78864,7 @@ function createCronScheduler(opts) {
78275
78864
  const parsed = getParsed(task.cron);
78276
78865
  const seen = lastSeenAt.get(task.id);
78277
78866
  const persistedCursor = task.lastFiredAt !== void 0 && Number.isFinite(task.lastFiredAt) && task.lastFiredAt <= clocks.wallNow() ? task.lastFiredAt : void 0;
78278
- const cursor = seen !== void 0 ? seen : persistedCursor !== void 0 ? persistedCursor : void 0;
78867
+ const cursor = seen ?? persistedCursor ?? void 0;
78279
78868
  return computeJitteredNext(task, parsed, cursor !== void 0 && cursor > task.createdAt ? cursor : task.createdAt);
78280
78869
  } catch (error) {
78281
78870
  debugLog(`getNextFireFor skipping task ${task.id}: ${error instanceof Error ? error.message : String(error)}`);
@@ -78371,7 +78960,9 @@ var CronManager = class {
78371
78960
  source: () => this.store.list(),
78372
78961
  isIdle: () => !agent.turn.hasActiveTurn,
78373
78962
  isKilled: () => process.env["SCREAM_DISABLE_CRON"] === "1",
78374
- onFire: (task, ctx) => this.handleFire(task, ctx),
78963
+ onFire: (task, ctx) => {
78964
+ this.handleFire(task, ctx);
78965
+ },
78375
78966
  removeOneShot: (id) => {
78376
78967
  this.removeTasks([id]);
78377
78968
  },
@@ -78464,8 +79055,8 @@ var CronManager = class {
78464
79055
  */
78465
79056
  persistEnqueue(id, work) {
78466
79057
  if (this.persistStore === void 0) return;
78467
- const next = (this.persistQueues.get(id) ?? Promise.resolve()).catch(() => {}).then(() => work()).catch((err) => {
78468
- this.agent.log?.warn?.("cron persist failed", err);
79058
+ const next = (this.persistQueues.get(id) ?? Promise.resolve()).catch(() => {}).then(() => work()).catch((error) => {
79059
+ this.agent.log?.warn?.("cron persist failed", error);
78469
79060
  }).finally(() => {
78470
79061
  if (this.persistQueues.get(id) === next) this.persistQueues.delete(id);
78471
79062
  });
@@ -78911,10 +79502,12 @@ var ContextMemory = class {
78911
79502
  if (removedUserCount >= count) break;
78912
79503
  }
78913
79504
  }
78914
- this.openSteps.clear();
78915
- this.pendingToolResultIds.clear();
78916
- this.deferredMessages = [];
78917
- this.agent.emitStatusUpdated();
79505
+ for (let i = this._history.length - 1; i >= 0; i--) {
79506
+ if (this._history[i]?.origin?.kind !== "injection") continue;
79507
+ const prev = this._history[i - 1];
79508
+ const next = this._history[i + 1];
79509
+ if (prev?.origin?.kind === "injection" || next?.origin?.kind === "injection" || i === 0) this._history.splice(i, 1);
79510
+ }
78918
79511
  if (!this.agent.records.restoring && (stoppedAtBoundary || removedUserCount < count)) {}
78919
79512
  }
78920
79513
  applyCompaction(summary) {
@@ -78937,6 +79530,7 @@ var ContextMemory = class {
78937
79530
  this.tokenCountCoveredMessageCount = this._history.length;
78938
79531
  this.agent.injection.onContextCompacted(summary.compactedCount);
78939
79532
  this.agent.emitStatusUpdated();
79533
+ this.agent.microCompaction.reset();
78940
79534
  }
78941
79535
  data() {
78942
79536
  return {
@@ -79068,8 +79662,8 @@ function toolResultOutputForModel(result) {
79068
79662
  }
79069
79663
  /** Truncate a plain-text tool output that exceeds MAX_TOOL_RESULT_TOKENS. */
79070
79664
  function truncateToolOutput(text) {
79071
- if (estimateTokens(text) <= MAX_TOOL_RESULT_TOKENS) return text;
79072
- const budget = MAX_TOOL_RESULT_TOKENS - estimateTokens(TOOL_TRUNCATION_NOTICE);
79665
+ if (estimateTokens$1(text) <= MAX_TOOL_RESULT_TOKENS) return text;
79666
+ const budget = MAX_TOOL_RESULT_TOKENS - estimateTokens$1(TOOL_TRUNCATION_NOTICE);
79073
79667
  if (budget <= 0) return TOOL_TRUNCATION_NOTICE.trim();
79074
79668
  let kept = "";
79075
79669
  let tokens = 0;
@@ -79084,9 +79678,9 @@ function truncateToolOutput(text) {
79084
79678
  /** Truncate oversized text parts in a ContentPart array. */
79085
79679
  function truncateContentParts(parts) {
79086
79680
  let totalTokens = 0;
79087
- for (const p of parts) if (p.type === "text") totalTokens += estimateTokens(p.text);
79681
+ for (const p of parts) if (p.type === "text") totalTokens += estimateTokens$1(p.text);
79088
79682
  if (totalTokens <= MAX_TOOL_RESULT_TOKENS) return [...parts];
79089
- const budget = MAX_TOOL_RESULT_TOKENS - estimateTokens(TOOL_TRUNCATION_NOTICE);
79683
+ const budget = MAX_TOOL_RESULT_TOKENS - estimateTokens$1(TOOL_TRUNCATION_NOTICE);
79090
79684
  if (budget <= 0) return [{
79091
79685
  type: "text",
79092
79686
  text: TOOL_TRUNCATION_NOTICE.trim()
@@ -79098,7 +79692,7 @@ function truncateContentParts(parts) {
79098
79692
  result.push(p);
79099
79693
  continue;
79100
79694
  }
79101
- const partTokens = estimateTokens(p.text);
79695
+ const partTokens = estimateTokens$1(p.text);
79102
79696
  if (used + partTokens <= budget) {
79103
79697
  result.push(p);
79104
79698
  used += partTokens;
@@ -79520,15 +80114,25 @@ var DynamicInjector = class {
79520
80114
  };
79521
80115
  //#endregion
79522
80116
  //#region ../../packages/agent-core/src/agent/injection/goal.ts
80117
+ const GOAL_REMINDER_INTERVAL = 5;
79523
80118
  var GoalInjector = class extends DynamicInjector {
79524
80119
  injectionVariant = "goal";
79525
80120
  getInjection() {
79526
80121
  const goal = this.agent.goal.getGoal().goal;
79527
80122
  if (goal === null) return void 0;
80123
+ if (this.injectedAt !== null) {
80124
+ if (this.countAssistantTurnsSince(this.injectedAt) < GOAL_REMINDER_INTERVAL) return void 0;
80125
+ }
79528
80126
  if (goal.status === "active") return buildGoalReminder(goal);
79529
80127
  if (goal.status === "blocked") return buildBlockedNote(goal);
79530
80128
  if (goal.status === "paused") return buildPausedNote(goal);
79531
80129
  }
80130
+ countAssistantTurnsSince(from) {
80131
+ let count = 0;
80132
+ const history = this.agent.context.history;
80133
+ for (let i = from; i < history.length; i++) if (history[i]?.role === "assistant") count++;
80134
+ return count;
80135
+ }
79532
80136
  };
79533
80137
  function buildBlockedNote(goal) {
79534
80138
  const reason = goal.terminalReason;
@@ -79932,11 +80536,15 @@ var WolfPackModeInjector = class extends DynamicInjector {
79932
80536
  };
79933
80537
  //#endregion
79934
80538
  //#region ../../packages/agent-core/src/agent/injection/working-set.ts
80539
+ const WORKING_SET_REMINDER_INTERVAL = 3;
79935
80540
  var WorkingSetInjector = class extends DynamicInjector {
79936
80541
  injectionVariant = "working-set";
79937
80542
  getInjection() {
79938
80543
  const paths = this.agent.workingSet.getPaths();
79939
80544
  if (paths.length === 0) return void 0;
80545
+ if (this.injectedAt !== null) {
80546
+ if (this.countAssistantTurnsSince(this.injectedAt) < WORKING_SET_REMINDER_INTERVAL) return void 0;
80547
+ }
79940
80548
  return [
79941
80549
  "## Working Set",
79942
80550
  "",
@@ -79946,6 +80554,12 @@ var WorkingSetInjector = class extends DynamicInjector {
79946
80554
  "优先检查这些文件,避免重复读取未修改文件。"
79947
80555
  ].join("\n");
79948
80556
  }
80557
+ countAssistantTurnsSince(from) {
80558
+ let count = 0;
80559
+ const history = this.agent.context.history;
80560
+ for (let i = from; i < history.length; i++) if (history[i]?.role === "assistant") count++;
80561
+ return count;
80562
+ }
79949
80563
  };
79950
80564
  //#endregion
79951
80565
  //#region ../../packages/agent-core/src/agent/injection/manager.ts
@@ -81203,21 +81817,14 @@ var WolfPackMode = class {
81203
81817
  //#region ../../packages/agent-core/src/agent/session-memory.ts
81204
81818
  const MAX_EVENTS = 50;
81205
81819
  const MAX_SUMMARY_LENGTH = 1500;
81206
- /**
81207
- * In-memory session running notes.
81208
- *
81209
- * Tracks tool executions and errors during the current session so a brief
81210
- * summary can be injected after compaction, preventing the model from losing
81211
- * its bearings when detailed conversation history is compressed away.
81212
- */
81213
81820
  var SessionMemory = class {
81214
81821
  agent;
81215
81822
  events = [];
81216
- lastInjectedStep = -1;
81823
+ lastInjectedSeq = -1;
81824
+ nextSeq = 1;
81217
81825
  constructor(agent) {
81218
81826
  this.agent = agent;
81219
81827
  }
81220
- /** Record a tool execution (success or failure). */
81221
81828
  recordToolExecution(toolName, argsSummary, isError, step) {
81222
81829
  this.events.push({
81223
81830
  type: "tool_execution",
@@ -81225,26 +81832,26 @@ var SessionMemory = class {
81225
81832
  argsSummary,
81226
81833
  isError,
81227
81834
  timestamp: Date.now(),
81228
- step
81835
+ step,
81836
+ seq: this.nextSeq++
81229
81837
  });
81230
81838
  if (this.events.length > MAX_EVENTS) this.events = this.events.slice(-50);
81231
81839
  }
81232
- /** Record a session-level error. */
81233
81840
  recordError(message, step) {
81234
81841
  this.events.push({
81235
81842
  type: "error",
81236
81843
  message,
81237
81844
  isError: true,
81238
81845
  timestamp: Date.now(),
81239
- step
81846
+ step,
81847
+ seq: this.nextSeq++
81240
81848
  });
81241
81849
  if (this.events.length > MAX_EVENTS) this.events = this.events.slice(-50);
81242
81850
  }
81243
- /** Build a markdown summary of recent activity since the last injection. */
81244
81851
  getSessionSummary() {
81245
- const newEvents = this.events.filter((e) => e.step > this.lastInjectedStep);
81852
+ const newEvents = this.events.filter((e) => e.seq > this.lastInjectedSeq);
81246
81853
  if (newEvents.length === 0) return "";
81247
- this.lastInjectedStep = this.events[this.events.length - 1]?.step ?? this.lastInjectedStep;
81854
+ this.lastInjectedSeq = this.events.at(-1)?.seq ?? this.lastInjectedSeq;
81248
81855
  const recent = newEvents.slice(-15);
81249
81856
  const toolExecs = recent.filter((e) => e.type === "tool_execution");
81250
81857
  const errors = recent.filter((e) => e.type === "error");
@@ -81267,10 +81874,10 @@ var SessionMemory = class {
81267
81874
  const joined = lines.join("\n");
81268
81875
  return joined.length > MAX_SUMMARY_LENGTH ? joined.slice(0, MAX_SUMMARY_LENGTH - 3) + "..." : joined;
81269
81876
  }
81270
- /** Reset for a new session. */
81271
81877
  clear() {
81272
81878
  this.events.length = 0;
81273
- this.lastInjectedStep = -1;
81879
+ this.lastInjectedSeq = -1;
81880
+ this.nextSeq = 1;
81274
81881
  }
81275
81882
  };
81276
81883
  const VERIFICATION_DEDUP_MS = 6e4;
@@ -81812,7 +82419,7 @@ var BlobStore = class {
81812
82419
  if (typeof url !== "string") continue;
81813
82420
  const newUrl = await this.maybeOffloadString(url);
81814
82421
  if (newUrl === url) continue;
81815
- if (updated === void 0) updated = { ...part };
82422
+ updated ??= { ...part };
81816
82423
  updated[key] = {
81817
82424
  ...value,
81818
82425
  url: newUrl
@@ -95505,6 +96112,7 @@ var TurnFlow = class {
95505
96112
  });
95506
96113
  if (turnId !== void 0 && turnId !== this.currentId) return;
95507
96114
  const cancelReason = reason ?? userCancellationReason();
96115
+ this.agent.usage.endTurn();
95508
96116
  this.abortTurn(cancelReason);
95509
96117
  this.agent.subagentHost?.cancelAll(cancelReason);
95510
96118
  }
@@ -95613,7 +96221,6 @@ var TurnFlow = class {
95613
96221
  turnId,
95614
96222
  origin
95615
96223
  });
95616
- this.agent.context.appendUserMessage(input, origin);
95617
96224
  const ended = {
95618
96225
  type: "turn.ended",
95619
96226
  turnId,
@@ -95645,6 +96252,7 @@ var TurnFlow = class {
95645
96252
  this.turnStartWorkingSetPathCount = this.agent.workingSet.getPaths().length;
95646
96253
  this.turnStartVerificationCount = this.agent.workingSet.getVerificationCount();
95647
96254
  this.currentStepByTurn.set(turnId, 0);
96255
+ this.currentStep = 0;
95648
96256
  this.agent.fullCompaction.resetForTurn();
95649
96257
  this.agent.injection.resetForTurn();
95650
96258
  this.agent.usage.beginTurn();
@@ -95793,11 +96401,13 @@ var TurnFlow = class {
95793
96401
  kind: "system_trigger",
95794
96402
  name: "todo_suggested"
95795
96403
  });
95796
- const sessionSummary = this.agent.sessionMemory.getSessionSummary();
95797
- if (sessionSummary.length > 0) this.agent.context.appendSystemReminder(sessionSummary, {
95798
- kind: "injection",
95799
- variant: "session_memory"
95800
- });
96404
+ if (stepNumber === 1 || this.agent.fullCompaction.shouldInjectSessionSummary()) {
96405
+ const sessionSummary = this.agent.sessionMemory.getSessionSummary();
96406
+ if (sessionSummary.length > 0) this.agent.context.appendSystemReminder(sessionSummary, {
96407
+ kind: "injection",
96408
+ variant: "session_memory"
96409
+ });
96410
+ }
95801
96411
  if (stepNumber === 1 && this.agent.dreamTracker.shouldSuggest()) this.agent.context.appendSystemReminder(this.agent.dreamTracker.getSuggestionMessage(), {
95802
96412
  kind: "injection",
95803
96413
  variant: "dream_suggestion"
@@ -95955,6 +96565,7 @@ var TurnFlow = class {
95955
96565
  }
95956
96566
  }
95957
96567
  }
96568
+ /** Track files touched by builtin tools. Bash-modified files are NOT tracked. */
95958
96569
  recordWorkingSetPaths(toolName, args, turnId) {
95959
96570
  const workingSet = this.agent.workingSet;
95960
96571
  if (toolName === "Read" || toolName === "ReadGroup" || toolName === "ReadMediaFile") {
@@ -96040,7 +96651,23 @@ var TurnFlow = class {
96040
96651
  /(^|;\s*|&&\s*)\s*pnp[ms]\s+add\s+(--global\s+)?typescript/,
96041
96652
  /(^|;\s*|&&\s*)\s*pnp[ms]\s+add\s+(--global\s+)?tsx/,
96042
96653
  /(^|;\s*|&&\s*)\s*yarn\s+add\s+(--dev\s+)?typescript/,
96043
- /(^|;\s*|&&\s*)\s*yarn\s+add\s+(--dev\s+)?tsx/
96654
+ /(^|;\s*|&&\s*)\s*yarn\s+add\s+(--dev\s+)?tsx/,
96655
+ /\bgit\s+status\b/,
96656
+ /\bgit\s+diff\b/,
96657
+ /\bgit\s+log\b/,
96658
+ /\bgrep\s+/,
96659
+ /\brg\s+/,
96660
+ /\bnode\s+-e\s+/,
96661
+ /\bpython\b/,
96662
+ /\bpython3\b/,
96663
+ /\bwc\s+/,
96664
+ /\bsort\s+/,
96665
+ /\buniq\b/,
96666
+ /\bdiff\s+/,
96667
+ /\bfile\s+/,
96668
+ /\bstat\s+/,
96669
+ /\bdf\s+/,
96670
+ /\bdu\s+/
96044
96671
  ].some((pattern) => pattern.test(normalized));
96045
96672
  }
96046
96673
  };
@@ -96289,7 +96916,7 @@ var LtodLLM = class {
96289
96916
  streamEndedAt = Date.now();
96290
96917
  };
96291
96918
  const markStreamOutput = () => {
96292
- if (firstChunkAt === void 0) firstChunkAt = Date.now();
96919
+ firstChunkAt ??= Date.now();
96293
96920
  };
96294
96921
  const callbacks = buildLtodCallbacks(params, markStreamOutput);
96295
96922
  const effectiveProvider = applyCompletionBudget({
@@ -96613,7 +97240,7 @@ var Agent = class {
96613
97240
  this.logLlmConfigIfChanged(context, configMetadata, buildLlmConfigSignature(configMetadata, systemPrompt, tools));
96614
97241
  let partialMessageCount = 0;
96615
97242
  for (const message of history) if (message.partial === true) partialMessageCount += 1;
96616
- const requestMetadata = { estimatedInputTokens: estimateTokens(systemPrompt) + estimateTokensForMessages(history) + estimateTokensForTools(tools) };
97243
+ const requestMetadata = { estimatedInputTokens: estimateTokens$1(systemPrompt) + estimateTokensForMessages(history) + estimateTokensForTools(tools) };
96617
97244
  if (partialMessageCount > 0) requestMetadata.partialMessageCount = partialMessageCount;
96618
97245
  this.log.info("llm request", {
96619
97246
  ...context,
@@ -96844,6 +97471,7 @@ var Agent = class {
96844
97471
  }
96845
97472
  const conversationContext = contextParts.join("\n\n");
96846
97473
  const system = conversationContext ? `${SIDE_QUESTION_SYSTEM}\n\n<conversation_context>\n${conversationContext}\n</conversation_context>` : SIDE_QUESTION_SYSTEM;
97474
+ if (!this.config.hasModel) return "No model configured. Run `scream config` or use `/model` to set a default model.";
96847
97475
  return (await this.generate(this.config.provider, system, [], [{
96848
97476
  role: "user",
96849
97477
  content: [{
@@ -100985,7 +101613,7 @@ function findParentMcpJsonPaths(cwd) {
100985
101613
  paths.push(join$1(dir, ".scream-code", "mcp.json"));
100986
101614
  dir = dirname$2(dir);
100987
101615
  }
100988
- return paths.reverse();
101616
+ return paths.toReversed();
100989
101617
  }
100990
101618
  /**
100991
101619
  * Load MCP server declarations from:
@@ -114909,7 +115537,7 @@ const LITE_RESULT_RE = /<a\s[^>]*?\bclass\s*=\s*["'][^"']*result-link[^"']*["'][
114909
115537
  const LITE_SNIPPET_RE = /<td\s[^>]*?\bclass\s*=\s*["'][^"']*result-snippet[^"']*["'][^>]*>([\s\S]*?)<\/td>/gi;
114910
115538
  /** Strip HTML tags and decode common HTML entities. */
114911
115539
  function stripHtmlAndDecode(text) {
114912
- return text.replace(/<[^>]*>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#x27;/g, "'").replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").trim();
115540
+ return text.replaceAll(/<[^>]*>/g, "").replaceAll("&amp;", "&").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&#x27;", "'").replaceAll("&#39;", "'").replaceAll("&nbsp;", " ").trim();
114913
115541
  }
114914
115542
  /** Unwrap a DuckDuckGo redirect URL to the real target if possible. */
114915
115543
  function unwrapDdgRedirect(url) {
@@ -117095,7 +117723,7 @@ function ccSessionToSummary(cc, agentSessionId, projectName) {
117095
117723
  const createdAt = parseCcTimestamp(cc.created_at);
117096
117724
  const updatedAt = parseCcTimestamp(cc.updated_at) ?? createdAt;
117097
117725
  if (createdAt === void 0 && updatedAt === void 0) return void 0;
117098
- const title = cc.name?.trim() || cc.id;
117726
+ const title = cc.name?.trim() ?? cc.id;
117099
117727
  const lastPrompt = lastPromptFromCcHistory(cc.history);
117100
117728
  const metadata = {
117101
117729
  source: "cc-connect",
@@ -117167,6 +117795,17 @@ var JianPathOutsideRootError = class extends JianError {
117167
117795
  this.name = "JianPathOutsideRootError";
117168
117796
  }
117169
117797
  };
117798
+ /** Thrown when process execution fails (spawn error, ENOENT, EACCES). */
117799
+ var JianExecError = class extends JianError {
117800
+ command;
117801
+ code;
117802
+ constructor(message, command, code) {
117803
+ super(message);
117804
+ this.command = command;
117805
+ this.code = code;
117806
+ this.name = "JianExecError";
117807
+ }
117808
+ };
117170
117809
  //#endregion
117171
117810
  //#region ../../packages/jian/src/environment.ts
117172
117811
  /**
@@ -117651,16 +118290,19 @@ var LocalProcess = class {
117651
118290
  "/PID",
117652
118291
  String(this.pid)
117653
118292
  ];
117654
- return new Promise((resolve) => {
118293
+ return new Promise((resolve, reject) => {
117655
118294
  const killer = spawn("taskkill", taskkillArgs, {
117656
118295
  stdio: "ignore",
117657
118296
  windowsHide: true
117658
118297
  });
117659
- const done = () => {
117660
- resolve();
117661
- };
117662
- killer.once("error", done);
117663
- killer.once("close", done);
118298
+ killer.once("error", (err) => {
118299
+ reject(/* @__PURE__ */ new Error(`taskkill failed to start: ${err.message}`));
118300
+ });
118301
+ killer.once("close", (code) => {
118302
+ if (code === 0) resolve();
118303
+ else reject(/* @__PURE__ */ new Error(`taskkill exited with code ${code ?? "null"}`));
118304
+ });
118305
+ killer.unref();
117664
118306
  });
117665
118307
  }
117666
118308
  try {
@@ -117720,6 +118362,14 @@ var LocalJian = class LocalJian {
117720
118362
  if (isWithinDirectory(resolvedPath, this._rootDir)) return;
117721
118363
  throw new JianPathOutsideRootError(`Path outside allowed root directory: ${resolvedPath}`, resolvedPath, this._rootDir);
117722
118364
  }
118365
+ /** Resolve path for sandboxed operations — lexical check + realpath. */
118366
+ async _resolveSandboxedPath(path) {
118367
+ const lexical = isAbsolute$1(path) ? normalize(path) : join$1(this._cwd, path);
118368
+ if (!isWithinDirectory(lexical, this._rootDir)) throw new JianPathOutsideRootError(`Path outside allowed root directory: ${lexical}`, lexical, this._rootDir);
118369
+ const realPath = await realpath(lexical);
118370
+ if (!isWithinDirectory(realPath, await realpath(this._rootDir))) throw new JianPathOutsideRootError(`Path outside allowed root directory (via symlink): ${lexical}`, lexical, this._rootDir);
118371
+ return realPath;
118372
+ }
117723
118373
  pathClass() {
117724
118374
  return isWindows ? "win32" : "posix";
117725
118375
  }
@@ -117742,13 +118392,14 @@ var LocalJian = class LocalJian {
117742
118392
  * call `process.chdir(x)` directly.
117743
118393
  */
117744
118394
  async chdir(path) {
117745
- const resolved = this._resolvePath(path);
118395
+ const resolved = this._rootDir !== void 0 ? await this._resolveSandboxedPath(path) : this._resolvePath(path);
117746
118396
  if (!(await stat(resolved)).isDirectory()) throw new Error(`Not a directory: ${resolved}`);
117747
118397
  this._cwd = resolved;
117748
118398
  }
117749
118399
  async stat(path, options) {
117750
- const resolved = this._resolvePath(path);
117751
- const s = options?.followSymlinks ?? true ? await stat(resolved) : await lstat(resolved);
118400
+ const followSymlinks = options?.followSymlinks ?? true;
118401
+ const resolved = this._rootDir !== void 0 && followSymlinks ? await this._resolveSandboxedPath(path) : this._resolvePath(path);
118402
+ const s = followSymlinks ? await stat(resolved) : await lstat(resolved);
117752
118403
  return {
117753
118404
  stMode: s.mode,
117754
118405
  stIno: s.ino,
@@ -117792,6 +118443,7 @@ var LocalJian = class LocalJian {
117792
118443
  }
117793
118444
  for (const entry of entries) {
117794
118445
  const fullPath = join$1(basePath, entry);
118446
+ if (this._rootDir && !isWithinDirectory(fullPath, this._rootDir)) continue;
117795
118447
  let entryStat;
117796
118448
  try {
117797
118449
  entryStat = await stat(fullPath);
@@ -117815,6 +118467,7 @@ var LocalJian = class LocalJian {
117815
118467
  for (const entry of entries) {
117816
118468
  if (!regex.test(entry)) continue;
117817
118469
  const fullPath = join$1(basePath, entry);
118470
+ if (this._rootDir && !isWithinDirectory(fullPath, this._rootDir)) continue;
117818
118471
  if (remainingParts.length === 0) yield fullPath;
117819
118472
  else {
117820
118473
  let entryStat;
@@ -117851,16 +118504,21 @@ var LocalJian = class LocalJian {
117851
118504
  return decodeTextWithErrors(await readFile(resolved), encoding, errors);
117852
118505
  }
117853
118506
  async *readLines(path, options) {
117854
- const resolved = this._resolvePath(path);
117855
- const encoding = options?.encoding ?? "utf-8";
117856
- const errors = options?.errors ?? "strict";
117857
- const lines = decodeTextWithErrors(await readFile(resolved), encoding, errors).split("\n");
117858
- for (let i = 0; i < lines.length; i++) {
117859
- const line = lines[i];
117860
- if (line === void 0) continue;
117861
- if (i < lines.length - 1) yield line + "\n";
117862
- else if (line !== "") yield line;
118507
+ const stream = createReadStream(this._resolvePath(path), {
118508
+ encoding: options?.encoding ?? "utf-8",
118509
+ highWaterMark: 64 * 1024
118510
+ });
118511
+ let remainder = "";
118512
+ try {
118513
+ for await (const chunk of stream) {
118514
+ const lines = (remainder + chunk).split("\n");
118515
+ remainder = lines.pop() ?? "";
118516
+ for (const line of lines) yield line + "\n";
118517
+ }
118518
+ } finally {
118519
+ stream.destroy();
117863
118520
  }
118521
+ if (remainder !== "") yield remainder;
117864
118522
  }
117865
118523
  async writeBytes(path, data) {
117866
118524
  await writeFile(this._resolvePath(path), data);
@@ -117913,7 +118571,12 @@ var LocalJian = class LocalJian {
117913
118571
  detached: !isWindows,
117914
118572
  env: buildSafeEnv(env)
117915
118573
  });
117916
- await waitForSpawn(child);
118574
+ try {
118575
+ await waitForSpawn(child);
118576
+ } catch (error) {
118577
+ if (error instanceof Error) throw new JianExecError(`Failed to spawn ${command}: ${error.message}`, command, error.code);
118578
+ throw error;
118579
+ }
117917
118580
  return new LocalProcess(child);
117918
118581
  }
117919
118582
  };
@@ -118923,26 +119586,28 @@ var SDKRpcClient = class {
118923
119586
  async getStatus(input) {
118924
119587
  const rpc = await this.getRpc();
118925
119588
  const agentId = this.interactiveAgentId;
118926
- const config = await rpc.getConfig({
118927
- sessionId: input.sessionId,
118928
- agentId
118929
- });
118930
- const context = await rpc.getContext({
118931
- sessionId: input.sessionId,
118932
- agentId
118933
- });
118934
- const permission = await rpc.getPermission({
118935
- sessionId: input.sessionId,
118936
- agentId
118937
- });
118938
- const plan = await rpc.getPlan({
118939
- sessionId: input.sessionId,
118940
- agentId
118941
- });
118942
- const usage = await rpc.getUsage({
118943
- sessionId: input.sessionId,
118944
- agentId
118945
- });
119589
+ const [config, context, permission, plan, usage] = await Promise.all([
119590
+ rpc.getConfig({
119591
+ sessionId: input.sessionId,
119592
+ agentId
119593
+ }),
119594
+ rpc.getContext({
119595
+ sessionId: input.sessionId,
119596
+ agentId
119597
+ }),
119598
+ rpc.getPermission({
119599
+ sessionId: input.sessionId,
119600
+ agentId
119601
+ }),
119602
+ rpc.getPlan({
119603
+ sessionId: input.sessionId,
119604
+ agentId
119605
+ }),
119606
+ rpc.getUsage({
119607
+ sessionId: input.sessionId,
119608
+ agentId
119609
+ })
119610
+ ]);
118946
119611
  const maxContextTokens = config.modelCapabilities?.max_context_tokens ?? 0;
118947
119612
  const contextTokens = context.tokenCount;
118948
119613
  const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0;
@@ -119085,7 +119750,9 @@ var SDKRpcClient = class {
119085
119750
  };
119086
119751
  }
119087
119752
  receiveEvent(event) {
119088
- for (const listener of this.eventListeners) listener(event);
119753
+ for (const listener of this.eventListeners) try {
119754
+ listener(event);
119755
+ } catch {}
119089
119756
  }
119090
119757
  setApprovalHandler(sessionId, handler) {
119091
119758
  if (handler === void 0) {
@@ -119206,19 +119873,24 @@ var Session = class {
119206
119873
  this.resumeState = options.resumeState ?? resumeStateFromSummary(options.summary);
119207
119874
  this.rpc = options.rpc;
119208
119875
  this.onClose = options.onClose;
119209
- const resumedMeta = this.resumeState?.sessionMetadata;
119210
- this.metadata = resumedMeta ?? options.summary?.metadata ?? {};
119211
- if (!this.metadata["custom"]) this.metadata["custom"] = {};
119876
+ const customBag = (this.resumeState?.sessionMetadata)?.custom ?? options.summary?.metadata ?? {};
119877
+ this.metadata = customBag;
119212
119878
  }
119213
119879
  getResumeState() {
119214
119880
  this.ensureOpen();
119215
119881
  return this.resumeState;
119216
119882
  }
119883
+ eventUnsubscribers = /* @__PURE__ */ new Set();
119217
119884
  onEvent(listener) {
119218
119885
  this.ensureOpen();
119219
- return this.rpc.onEvent((event) => {
119886
+ const unsub = this.rpc.onEvent((event) => {
119220
119887
  if (event.sessionId === this.id) listener(event);
119221
119888
  });
119889
+ this.eventUnsubscribers.add(unsub);
119890
+ return () => {
119891
+ unsub();
119892
+ this.eventUnsubscribers.delete(unsub);
119893
+ };
119222
119894
  }
119223
119895
  setApprovalHandler(handler) {
119224
119896
  this.ensureOpen();
@@ -119545,11 +120217,13 @@ var Session = class {
119545
120217
  }
119546
120218
  async close(options = {}) {
119547
120219
  if (this.closed) return;
119548
- if (options.extractMemories !== false) try {
119549
- const extract = this.extractMemoriesOnExit().catch(() => {});
119550
- await Promise.race([extract, new Promise((resolve) => setTimeout(resolve, 3e4))]);
119551
- } catch {}
120220
+ if (options.extractMemories !== false) {
120221
+ const extract = this.extractMemoriesOnExit();
120222
+ await Promise.race([extract.catch(() => {}), new Promise((r) => setTimeout(r, 3e4))]);
120223
+ }
119552
120224
  this.closed = true;
120225
+ for (const unsub of this.eventUnsubscribers) unsub();
120226
+ this.eventUnsubscribers.clear();
119553
120227
  try {
119554
120228
  await this.rpc.closeSession({ sessionId: this.id });
119555
120229
  } finally {
@@ -120067,11 +120741,11 @@ function detectMultiplexer() {
120067
120741
  }
120068
120742
  function detectShellEnvironment() {
120069
120743
  return {
120070
- term: process.env["TERM"] || void 0,
120071
- termProgram: process.env["TERM_PROGRAM"] || void 0,
120072
- termProgramVersion: process.env["TERM_PROGRAM_VERSION"] || void 0,
120744
+ term: process.env["TERM"] ?? void 0,
120745
+ termProgram: process.env["TERM_PROGRAM"] ?? void 0,
120746
+ termProgramVersion: process.env["TERM_PROGRAM_VERSION"] ?? void 0,
120073
120747
  multiplexer: detectMultiplexer(),
120074
- shell: process.env["SHELL"] || void 0
120748
+ shell: process.env["SHELL"] ?? void 0
120075
120749
  };
120076
120750
  }
120077
120751
  //#endregion
@@ -121283,6 +121957,19 @@ function formatErrorMessage(error) {
121283
121957
  if (isScreamError(error)) return `[${error.code}] ${error.message}`;
121284
121958
  return error instanceof Error ? error.message : String(error);
121285
121959
  }
121960
+ /**
121961
+ * Cap an error message body for inline transcript display. Drops blank lines,
121962
+ * keeps the first `maxLines` non-blank lines, and appends a `… (N more lines)`
121963
+ * hint when truncated. Full text is preserved in the persisted session — this
121964
+ * only bounds the live transcript render.
121965
+ */
121966
+ function truncateErrorMessage(message, maxLines = 8) {
121967
+ const lines = message.split("\n").filter((line) => line.trim().length > 0);
121968
+ if (lines.length <= maxLines) return lines.join("\n");
121969
+ const kept = lines.slice(0, maxLines);
121970
+ const remaining = lines.length - maxLines;
121971
+ return `${kept.join("\n")}\n… (${String(remaining)} more lines)`;
121972
+ }
121286
121973
  function stringValue(value) {
121287
121974
  return typeof value === "string" ? value : void 0;
121288
121975
  }
@@ -121408,7 +122095,7 @@ var ApiKeyInputDialogComponent = class extends Container {
121408
122095
  };
121409
122096
  //#endregion
121410
122097
  //#region src/tui/constant/symbols.ts
121411
- const STATUS_BULLET = " ";
122098
+ const STATUS_BULLET = " ";
121412
122099
  const USER_MESSAGE_BULLET = "┃ ";
121413
122100
  const FAILURE_MARK = "✗ ";
121414
122101
  //#endregion
@@ -122197,7 +122884,7 @@ async function handleDiyConfig(host) {
122197
122884
  const maxContextTokens = parseInt(maxContextStr, 10) || 131072;
122198
122885
  const thinking = await promptThinkingMode(host);
122199
122886
  if (thinking === void 0) return;
122200
- const providerId = `custom-${modelId.replace(/[^A-Za-z0-9._-]/g, "-")}`;
122887
+ const providerId = `custom-${modelId.replaceAll(/[^A-Za-z0-9._-]/g, "-")}`;
122201
122888
  const catalogModel = {
122202
122889
  id: modelId,
122203
122890
  name: modelId,
@@ -122588,7 +123275,7 @@ const darkColors = {
122588
123275
  const lightColors = {
122589
123276
  primary: light.green,
122590
123277
  accent: light.tangerine700,
122591
- planMode: "#00FFFF",
123278
+ planMode: "#00838F",
122592
123279
  text: light.gray900,
122593
123280
  textStrong: light.gray900,
122594
123281
  textDim: light.gray700,
@@ -124027,7 +124714,9 @@ async function showGoalStatus(host) {
124027
124714
  const panel = new GoalStatusMessageComponent(result.goal, host.state.theme.colors);
124028
124715
  host.state.transcriptContainer.addChild(panel);
124029
124716
  activeGoalPanel = panel;
124030
- activeGoalTimer = setTimeout(() => dismissGoalPanel(host), GOAL_STATUS_DISMISS_MS);
124717
+ activeGoalTimer = setTimeout(() => {
124718
+ dismissGoalPanel(host);
124719
+ }, GOAL_STATUS_DISMISS_MS);
124031
124720
  host.state.ui.requestRender();
124032
124721
  } catch (error) {
124033
124722
  const message = error instanceof Error ? error.message : String(error);
@@ -124787,10 +125476,109 @@ var SkillActivationComponent = class extends Container {
124787
125476
  }
124788
125477
  };
124789
125478
  //#endregion
125479
+ //#region src/tui/utils/speed-tracker.ts
125480
+ /**
125481
+ * Streaming-speed gauge for the live thinking indicator.
125482
+ *
125483
+ * Ported from oh-my-pi `packages/coding-agent/src/modes/components/assistant-message.ts:120-178`.
125484
+ * scream-code adaptation: the provider does not report cumulative token counts
125485
+ * during streaming (only character deltas), so instantaneous tok/s is estimated
125486
+ * from delta length via {@link CHARS_PER_TOKEN_ESTIMATE}. The badge is a progress
125487
+ * indicator, not a precise meter.
125488
+ */
125489
+ /** Rolling window (ms) over which streaming-rate observations are averaged. */
125490
+ const SPEED_WINDOW_MS = 3e3;
125491
+ /**
125492
+ * Chars-per-token estimate for converting character deltas to an approximate
125493
+ * token count. 2.5 is a middle ground between English (~4 chars/token) and
125494
+ * Chinese (~1 char/token). Pure Chinese underestimates ~2.5x, pure English
125495
+ * overestimates ~1.6x — acceptable for a progress indicator.
125496
+ */
125497
+ const CHARS_PER_TOKEN_ESTIMATE = 2.5;
125498
+ var SpeedTracker = class {
125499
+ observations = [];
125500
+ prune(now) {
125501
+ const threshold = now - SPEED_WINDOW_MS;
125502
+ while (this.observations.length > 0 && this.observations[0].time < threshold) this.observations.shift();
125503
+ }
125504
+ /**
125505
+ * Record one instantaneous tok/s reading, clamped to {@link SPEED_MAX} so a
125506
+ * single oversized delta can't poison the windowed average. Non-finite or
125507
+ * negative rates are ignored.
125508
+ */
125509
+ observe(rate, now = performance.now()) {
125510
+ if (!Number.isFinite(rate) || rate < 0) return;
125511
+ this.observations.push({
125512
+ time: now,
125513
+ rate: Math.min(rate, 200)
125514
+ });
125515
+ this.prune(now);
125516
+ }
125517
+ /** Windowed-average tok/s; 0 once observations age out of the window. */
125518
+ getSpeed(now = performance.now()) {
125519
+ this.prune(now);
125520
+ if (this.observations.length === 0) return 0;
125521
+ let sum = 0;
125522
+ for (const o of this.observations) sum += o.rate;
125523
+ return sum / this.observations.length;
125524
+ }
125525
+ reset() {
125526
+ this.observations = [];
125527
+ }
125528
+ };
125529
+ /**
125530
+ * One gauge for the whole session. Only the single live thinking block feeds it
125531
+ * (via {@link StreamingUIController.appendThinkingDelta} / {@link appendAssistantDelta}),
125532
+ * and only the live {@link ThinkingComponent} reads it. Reset on turn boundaries
125533
+ * so a previous turn's trailing rate doesn't leak onto a fresh block.
125534
+ */
125535
+ const sharedSpeedTracker = new SpeedTracker();
125536
+ function getSharedSpeedTracker() {
125537
+ return sharedSpeedTracker;
125538
+ }
125539
+ /**
125540
+ * Linear-interpolate two `#rrggbb` colors in sRGB space. `t` clamps to [0,1]:
125541
+ * `t = 0` → `from`, `t = 1` → `to`. Drives the streaming speed badge, fading
125542
+ * from a dim gray toward the theme accent as tok/s rises.
125543
+ */
125544
+ function lerpHex(from, to, t) {
125545
+ const k = t < 0 ? 0 : t > 1 ? 1 : t;
125546
+ const fr = Number.parseInt(from.slice(1, 3), 16);
125547
+ const fg = Number.parseInt(from.slice(3, 5), 16);
125548
+ const fb = Number.parseInt(from.slice(5, 7), 16);
125549
+ const tr = Number.parseInt(to.slice(1, 3), 16);
125550
+ const tg = Number.parseInt(to.slice(3, 5), 16);
125551
+ const tb = Number.parseInt(to.slice(5, 7), 16);
125552
+ const r = Math.round(fr + (tr - fr) * k);
125553
+ const g = Math.round(fg + (tg - fg) * k);
125554
+ const b = Math.round(fb + (tb - fb) * k);
125555
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
125556
+ }
125557
+ /**
125558
+ * Estimate token count from a character delta. At least 1 to avoid zero-rate
125559
+ * observations when a tiny delta arrives.
125560
+ */
125561
+ function estimateTokens(delta) {
125562
+ return Math.max(1, Math.round(delta.length / CHARS_PER_TOKEN_ESTIMATE));
125563
+ }
125564
+ /**
125565
+ * Ease the normalized speed ratio [0,1] for color interpolation. Uses smoothstep
125566
+ * (zero derivative at both endpoints) so the badge stays mostly gray at low
125567
+ * rates — subtle rather than distracting — and only reaches the full accent
125568
+ * color at high rates. Smoother than sqrt, whose infinite derivative at t=0
125569
+ * makes the color jump toward accent as soon as any tokens flow.
125570
+ */
125571
+ function easeSpeedRatio(ratio) {
125572
+ const t = ratio < 0 ? 0 : ratio > 1 ? 1 : ratio;
125573
+ return t * t * (3 - 2 * t);
125574
+ }
125575
+ //#endregion
124790
125576
  //#region src/tui/components/messages/thinking.ts
124791
125577
  var ThinkingComponent = class {
124792
125578
  text;
124793
125579
  color;
125580
+ dimColor;
125581
+ accentColor;
124794
125582
  showMarker;
124795
125583
  mode;
124796
125584
  expanded = false;
@@ -124803,6 +125591,8 @@ var ThinkingComponent = class {
124803
125591
  constructor(text, colors, showMarker = true, mode = "finalized", ui) {
124804
125592
  this.text = text;
124805
125593
  this.color = colors.roleThinking;
125594
+ this.dimColor = colors.textDim;
125595
+ this.accentColor = colors.primary;
124806
125596
  this.showMarker = showMarker;
124807
125597
  this.mode = mode;
124808
125598
  this.ui = ui;
@@ -124845,9 +125635,12 @@ var ThinkingComponent = class {
124845
125635
  const contentLines = this.text.length > 0 ? this.textComponent.render(contentWidth) : [""];
124846
125636
  if (this.mode === "live") {
124847
125637
  const visibleLines = contentLines.length > 2 ? contentLines.slice(contentLines.length - 2) : contentLines;
125638
+ const spinner = chalk.hex(this.color)(`${BRAILLE_SPINNER_FRAMES[this.spinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0]} `);
125639
+ const rate = Math.min(200, getSharedSpeedTracker().getSpeed());
125640
+ const rateSuffix = rate > .05 ? chalk.hex(lerpHex(this.dimColor, this.accentColor, easeSpeedRatio(rate / 200)))(` · ${rate.toFixed(1)} toks/s`) : "";
124848
125641
  return [
124849
125642
  "",
124850
- chalk.hex(this.color)(`${BRAILLE_SPINNER_FRAMES[this.spinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0]} `) + chalk.hex(this.color)("思考中..."),
125643
+ spinner + chalk.hex(this.color)("思考中...") + rateSuffix,
124851
125644
  ...visibleLines.map((line) => " " + line)
124852
125645
  ];
124853
125646
  }
@@ -124949,6 +125742,66 @@ function makeDiffStyles(colors) {
124949
125742
  meta: (s) => chalk.hex(colors.diffMeta)(s)
124950
125743
  };
124951
125744
  }
125745
+ /**
125746
+ * Compute word-level diff between two single lines and highlight the changed
125747
+ * words with `chalk.inverse()`. Only the first removed/added part has its
125748
+ * leading whitespace stripped, so indentation isn't highlighted.
125749
+ *
125750
+ * Ported from oh-my-pi `packages/coding-agent/src/modes/components/diff.ts:55-95`.
125751
+ * Returns the two lines with inline inverse highlighting applied; caller wraps
125752
+ * them in the usual add/del gutter + line color.
125753
+ */
125754
+ function renderIntraLineDiff(oldContent, newContent) {
125755
+ const wordDiff = diffWords(oldContent, newContent);
125756
+ let removedLine = "";
125757
+ let addedLine = "";
125758
+ let isFirstRemoved = true;
125759
+ let isFirstAdded = true;
125760
+ for (const part of wordDiff) if (part.removed) {
125761
+ let value = part.value;
125762
+ if (isFirstRemoved) {
125763
+ const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
125764
+ value = value.slice(leadingWs.length);
125765
+ removedLine += leadingWs;
125766
+ isFirstRemoved = false;
125767
+ }
125768
+ if (value) removedLine += chalk.inverse(value);
125769
+ } else if (part.added) {
125770
+ let value = part.value;
125771
+ if (isFirstAdded) {
125772
+ const leadingWs = value.match(/^(\s*)/)?.[1] ?? "";
125773
+ value = value.slice(leadingWs.length);
125774
+ addedLine += leadingWs;
125775
+ isFirstAdded = false;
125776
+ }
125777
+ if (value) addedLine += chalk.inverse(value);
125778
+ } else {
125779
+ removedLine += part.value;
125780
+ addedLine += part.value;
125781
+ }
125782
+ return {
125783
+ removedLine,
125784
+ addedLine
125785
+ };
125786
+ }
125787
+ /**
125788
+ * Check whether a delete line at index `i` and an add line at index `i+1`
125789
+ * form an isolated single-line replacement — the only case where intra-line
125790
+ * word diff is meaningful. We require both sides to be single-line blocks
125791
+ * (prev of delete is not delete; next of add is not add) so multi-line
125792
+ * delete/add blocks stay as whole-line coloring.
125793
+ */
125794
+ function isIsolatedSingleLinePair(lines, i) {
125795
+ const curr = lines[i];
125796
+ const next = lines[i + 1];
125797
+ if (curr === void 0 || next === void 0) return false;
125798
+ if (curr.kind !== "delete" || next.kind !== "add") return false;
125799
+ const prev = lines[i - 1];
125800
+ if (prev !== void 0 && prev.kind === "delete") return false;
125801
+ const after = lines[i + 2];
125802
+ if (after !== void 0 && after.kind === "add") return false;
125803
+ return true;
125804
+ }
124952
125805
  function computeDiffLines(oldText, newText, oldStart = 1, newStart = 1, isIncomplete = false) {
124953
125806
  const oldLines = oldText ? oldText.split("\n") : [];
124954
125807
  const newLines = newText ? newText.split("\n") : [];
@@ -125005,10 +125858,22 @@ function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, o
125005
125858
  header += path;
125006
125859
  output.push(header);
125007
125860
  const shown = maxLines !== void 0 && maxLines >= 0 && changedLines.length > maxLines ? changedLines.slice(0, maxLines) : changedLines;
125008
- for (const line of shown) {
125861
+ let i = 0;
125862
+ while (i < shown.length) {
125863
+ if (isIsolatedSingleLinePair(shown, i)) {
125864
+ const line = shown[i];
125865
+ const next = shown[i + 1];
125866
+ const { removedLine, addedLine } = renderIntraLineDiff(line.code, next.code);
125867
+ output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + s.del("- " + removedLine));
125868
+ output.push(s.gutter(String(next.lineNum).padStart(4) + " ") + s.add("+ " + addedLine));
125869
+ i += 2;
125870
+ continue;
125871
+ }
125872
+ const line = shown[i];
125009
125873
  const marker = line.kind === "add" ? "+" : "-";
125010
125874
  const color = line.kind === "add" ? s.add : s.del;
125011
125875
  output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(marker + " " + line.code));
125876
+ i += 1;
125012
125877
  }
125013
125878
  const hidden = changedLines.length - shown.length;
125014
125879
  if (hidden > 0) output.push(s.meta(` … ${String(hidden)} more change${hidden > 1 ? "s" : ""} hidden (ctrl+o to expand)`));
@@ -125107,16 +125972,35 @@ function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
125107
125972
  body++;
125108
125973
  }
125109
125974
  }
125110
- for (let i = cluster.start; i <= cluster.end; i++) {
125975
+ let i = cluster.start;
125976
+ while (i <= cluster.end) {
125111
125977
  if (body >= cap) {
125112
125978
  truncated = true;
125113
125979
  break outer;
125114
125980
  }
125115
125981
  const line = diffLines[i];
125982
+ if (i + 1 <= cluster.end && isIsolatedSingleLinePair(diffLines, i)) {
125983
+ const next = diffLines[i + 1];
125984
+ const { removedLine, addedLine } = renderIntraLineDiff(line.code, next.code);
125985
+ output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + s.del("- " + removedLine));
125986
+ body++;
125987
+ if (body >= cap) {
125988
+ truncated = true;
125989
+ prevEnd = i;
125990
+ break outer;
125991
+ }
125992
+ output.push(s.gutter(String(next.lineNum).padStart(4) + " ") + s.add("+ " + addedLine));
125993
+ body++;
125994
+ shownChanges += 2;
125995
+ prevEnd = i + 1;
125996
+ i += 2;
125997
+ continue;
125998
+ }
125116
125999
  output.push(formatDiffRow(line, s));
125117
126000
  body++;
125118
126001
  if (line.kind !== "context") shownChanges++;
125119
126002
  prevEnd = i;
126003
+ i += 1;
125120
126004
  }
125121
126005
  }
125122
126006
  if (truncated) {
@@ -125351,7 +126235,66 @@ const shellExecutionResultRenderer = (toolCall, result, ctx) => [new ShellExecut
125351
126235
  commandPreviewLines: void 0
125352
126236
  })];
125353
126237
  //#endregion
126238
+ //#region src/tui/components/media/image-thumbnail.ts
126239
+ /**
126240
+ * Transcript-side rendering of a pasted image.
126241
+ *
126242
+ * On terminals that speak the Kitty graphics protocol or iTerm2 inline
126243
+ * image protocol (detected by pi-tui's `getCapabilities()`), we show
126244
+ * the actual image. Everywhere else we fall back to a one-line text
126245
+ * marker matching the placeholder the user sees in the input box —
126246
+ * this keeps the transcript readable on Terminal.app / Linux default
126247
+ * terminals / `script` recordings without extra chrome.
126248
+ *
126249
+ * Height is capped at ~12 rows so a single screenshot can't monopolize
126250
+ * the viewport; pi-tui handles proportional scaling internally.
126251
+ */
126252
+ const MAX_IMAGE_ROWS = 12;
126253
+ const MAX_IMAGE_WIDTH = 40;
126254
+ function createImageComponent(opts, colors) {
126255
+ const caps = getCapabilities();
126256
+ if (!(caps.images === "kitty" || caps.images === "iterm2")) {
126257
+ const label = opts.filename ?? `[${opts.mime}]`;
126258
+ return new Text(chalk.hex(colors.accent)(label), 0, 0);
126259
+ }
126260
+ return new Image(opts.base64, opts.mime, { fallbackColor: (s) => chalk.hex(colors.textDim)(s) }, {
126261
+ maxHeightCells: MAX_IMAGE_ROWS,
126262
+ maxWidthCells: MAX_IMAGE_WIDTH,
126263
+ filename: opts.filename
126264
+ }, {
126265
+ widthPx: opts.width,
126266
+ heightPx: opts.height
126267
+ });
126268
+ }
126269
+ var ImageThumbnail = class extends Container {
126270
+ constructor(attachment, colors) {
126271
+ super();
126272
+ const base64 = Buffer.from(attachment.bytes).toString("base64");
126273
+ this.addChild(createImageComponent({
126274
+ base64,
126275
+ mime: attachment.mime,
126276
+ width: attachment.width,
126277
+ height: attachment.height,
126278
+ filename: attachment.placeholder
126279
+ }, colors));
126280
+ }
126281
+ };
126282
+ //#endregion
125354
126283
  //#region src/tui/components/messages/tool-renderers/media.ts
126284
+ /**
126285
+ * ReadMediaFile renderer.
126286
+ *
126287
+ * The ReadMediaFile tool `output` is the JSON-serialized array of
126288
+ * content parts the tool returned — which includes the full base64 of
126289
+ * the image/video. Dumping that string into the transcript blasts a
126290
+ * multi-screen blob of base64. This renderer parses the envelope and
126291
+ * surfaces just the human-readable bits (kind, path, mime, size) via
126292
+ * a header chip + a tiny expanded body. It never emits the base64.
126293
+ *
126294
+ * On error, or when the output isn't the expected media envelope, we
126295
+ * fall back to the truncated renderer so the user still sees the raw
126296
+ * message.
126297
+ */
125355
126298
  const PATH_TAG_RE = /^<(image|video)\s+path="([^"]+)">$/;
125356
126299
  const ORIGINAL_SIZE_RE = /original size\s+(\d+x\d+px)/;
125357
126300
  const DATA_URL_RE = /^data:([^;]+);base64,(.*)$/s;
@@ -125373,6 +126316,7 @@ function parseReadMediaOutput(output) {
125373
126316
  let path;
125374
126317
  let mimeType;
125375
126318
  let bytes;
126319
+ let base64;
125376
126320
  let url;
125377
126321
  let originalSize;
125378
126322
  let foundMedia = false;
@@ -125403,6 +126347,7 @@ function parseReadMediaOutput(output) {
125403
126347
  if (data && data[1] !== void 0 && data[2] !== void 0) {
125404
126348
  mimeType = data[1];
125405
126349
  bytes = bytesFromBase64(data[2]);
126350
+ base64 = data[2];
125406
126351
  } else url = u;
125407
126352
  }
125408
126353
  }
@@ -125413,6 +126358,7 @@ function parseReadMediaOutput(output) {
125413
126358
  if (path !== void 0) summary.path = path;
125414
126359
  if (mimeType !== void 0) summary.mimeType = mimeType;
125415
126360
  if (bytes !== void 0) summary.bytes = bytes;
126361
+ if (base64 !== void 0) summary.base64 = base64;
125416
126362
  if (url !== void 0) summary.url = url;
125417
126363
  if (originalSize !== void 0) summary.originalSize = originalSize;
125418
126364
  return summary;
@@ -125441,15 +126387,33 @@ const readMediaSummary = (toolCall, result, ctx) => {
125441
126387
  if (result.is_error) return renderTruncated(toolCall, result, ctx);
125442
126388
  const summary = parseReadMediaOutput(result.output);
125443
126389
  if (summary === null) return renderTruncated(toolCall, result, ctx);
125444
- if (!ctx.expanded) return [];
125445
126390
  const dim = chalk.dim;
125446
126391
  const out = [];
125447
- if (summary.path !== void 0) out.push(new Text(` ${dim(summary.path)}`, 0, 0));
125448
- const meta = metaSegments(summary);
125449
- const tail = [summary.kind];
125450
- if (meta.length > 0) tail.push(meta.join(", "));
125451
- if (summary.url !== void 0) tail.push(summary.url);
125452
- out.push(new Text(` ${dim(tail.join(" · "))}`, 0, 0));
126392
+ if (ctx.expanded) {
126393
+ if (summary.path !== void 0) out.push(new Text(` ${dim(summary.path)}`, 0, 0));
126394
+ const meta = metaSegments(summary);
126395
+ const tail = [summary.kind];
126396
+ if (meta.length > 0) tail.push(meta.join(", "));
126397
+ if (summary.url !== void 0) tail.push(summary.url);
126398
+ out.push(new Text(` ${dim(tail.join(" · "))}`, 0, 0));
126399
+ }
126400
+ if (summary.kind === "image" && summary.base64 !== void 0 && summary.mimeType !== void 0) {
126401
+ const caps = getCapabilities();
126402
+ const supportsInline = caps.images === "kitty" || caps.images === "iterm2";
126403
+ if (ctx.expanded || supportsInline) {
126404
+ const dims = getImageDimensions(summary.base64, summary.mimeType) ?? {
126405
+ widthPx: 800,
126406
+ heightPx: 600
126407
+ };
126408
+ out.push(createImageComponent({
126409
+ base64: summary.base64,
126410
+ mime: summary.mimeType,
126411
+ width: dims.widthPx,
126412
+ height: dims.heightPx,
126413
+ filename: summary.path
126414
+ }, ctx.colors));
126415
+ }
126416
+ }
125453
126417
  return out;
125454
126418
  };
125455
126419
  //#endregion
@@ -126868,42 +127832,6 @@ function formatActivityLine(verb, toolName, args, workspaceDir) {
126868
127832
  return keyArg ? `${verb} ${toolName} (${keyArg})` : `${verb} ${toolName}`;
126869
127833
  }
126870
127834
  //#endregion
126871
- //#region src/tui/components/media/image-thumbnail.ts
126872
- /**
126873
- * Transcript-side rendering of a pasted image.
126874
- *
126875
- * On terminals that speak the Kitty graphics protocol or iTerm2 inline
126876
- * image protocol (detected by pi-tui's `getCapabilities()`), we show
126877
- * the actual image. Everywhere else we fall back to a one-line text
126878
- * marker matching the placeholder the user sees in the input box —
126879
- * this keeps the transcript readable on Terminal.app / Linux default
126880
- * terminals / `script` recordings without extra chrome.
126881
- *
126882
- * Height is capped at ~12 rows so a single screenshot can't monopolize
126883
- * the viewport; pi-tui handles proportional scaling internally.
126884
- */
126885
- const MAX_IMAGE_ROWS = 12;
126886
- const MAX_IMAGE_WIDTH = 40;
126887
- var ImageThumbnail = class extends Container {
126888
- constructor(attachment, colors) {
126889
- super();
126890
- const caps = getCapabilities();
126891
- if (!(caps.images === "kitty" || caps.images === "iterm2")) {
126892
- this.addChild(new Text(chalk.hex(colors.accent)(attachment.placeholder), 0, 0));
126893
- return;
126894
- }
126895
- const image = new Image(Buffer.from(attachment.bytes).toString("base64"), attachment.mime, { fallbackColor: (s) => chalk.hex(colors.textDim)(s) }, {
126896
- maxHeightCells: MAX_IMAGE_ROWS,
126897
- maxWidthCells: MAX_IMAGE_WIDTH,
126898
- filename: attachment.placeholder
126899
- }, {
126900
- widthPx: attachment.width,
126901
- heightPx: attachment.height
126902
- });
126903
- this.addChild(image);
126904
- }
126905
- };
126906
- //#endregion
126907
127835
  //#region src/tui/components/messages/user-message.ts
126908
127836
  var UserMessageComponent = class {
126909
127837
  color;
@@ -127695,7 +128623,7 @@ async function loadServers(host) {
127695
128623
  }
127696
128624
  /** Replace newlines so error messages don't break single-line terminal rendering. */
127697
128625
  function sanitizeDesc(s) {
127698
- return s.replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim();
128626
+ return s.replaceAll(/\r?\n/g, " ").replaceAll(/\s+/g, " ").trim();
127699
128627
  }
127700
128628
  function buildRows(servers) {
127701
128629
  const rows = [];
@@ -127933,7 +128861,8 @@ var McpPickerComponent = class extends Container {
127933
128861
  this.selectedIndex = this.nextSelectable(this.selectedIndex);
127934
128862
  return;
127935
128863
  }
127936
- if (data === "d" || data === "D") {
128864
+ const key = printableChar(data);
128865
+ if (key === "d" || key === "D") {
127937
128866
  const row = this.rows[this.selectedIndex];
127938
128867
  if (row && row.kind === "installed" && row.status && row.status !== "__section" && row.status !== "__empty") this.onDelete(row);
127939
128868
  return;
@@ -128098,7 +129027,7 @@ function readConfiguredType() {
128098
129027
  }
128099
129028
  }
128100
129029
  function escapeSingleQuotes(str) {
128101
- return str.replace(/'/g, "\\'");
129030
+ return str.replaceAll("'", "\\'");
128102
129031
  }
128103
129032
  function generateConfig$1(platform) {
128104
129033
  const dir = dirname$1(CONFIG_PATH);
@@ -130137,11 +131066,18 @@ var EditorKeyboardController = class {
130137
131066
  }
130138
131067
  };
130139
131068
  //#endregion
131069
+ //#region src/tui/utils/sanitize.ts
131070
+ /** Replace tabs with spaces, keeping column alignment. Default tab width 4. */
131071
+ function replaceTabs(text, tabWidth = 4) {
131072
+ return text.replaceAll(" ", " ".repeat(tabWidth));
131073
+ }
131074
+ //#endregion
130140
131075
  //#region src/tui/components/messages/status-message.ts
130141
131076
  var StatusMessageComponent = class extends Container {
130142
131077
  constructor(content, colors, color) {
130143
131078
  super();
130144
- const text = color === void 0 ? chalk.hex(colors.textDim)(content) : chalk.hex(color)(content);
131079
+ const sanitized = replaceTabs(content);
131080
+ const text = color === void 0 ? chalk.hex(colors.textDim)(sanitized) : chalk.hex(color)(sanitized);
130145
131081
  this.addChild(new Text(` ${text}`, 0, 0));
130146
131082
  }
130147
131083
  };
@@ -130149,8 +131085,8 @@ var NoticeMessageComponent = class extends Container {
130149
131085
  constructor(title, detail, colors) {
130150
131086
  super();
130151
131087
  this.addChild(new Spacer(1));
130152
- this.addChild(new Text(` ${chalk.hex(colors.textStrong)(title)}`, 0, 0));
130153
- if (detail !== void 0 && detail.length > 0) this.addChild(new Text(` ${chalk.hex(colors.textDim)(detail)}`, 0, 0));
131088
+ this.addChild(new Text(` ${chalk.hex(colors.textStrong)(replaceTabs(title))}`, 0, 0));
131089
+ if (detail !== void 0 && detail.length > 0) this.addChild(new Text(` ${chalk.hex(colors.textDim)(replaceTabs(detail))}`, 0, 0));
130154
131090
  }
130155
131091
  };
130156
131092
  //#endregion
@@ -131964,6 +132900,7 @@ var StreamingUIController = class {
131964
132900
  _currentStep = 0;
131965
132901
  _assistantDraft = "";
131966
132902
  _thinkingDraft = "";
132903
+ lastDeltaAt;
131967
132904
  _thinkingEntry = void 0;
131968
132905
  _compactionEntry = void 0;
131969
132906
  _streamingBlock = null;
@@ -131996,11 +132933,30 @@ var StreamingUIController = class {
131996
132933
  appendThinkingDelta(delta) {
131997
132934
  this._thinkingDraft += delta;
131998
132935
  this.pendingThinkingFlush = true;
132936
+ this.recordSpeedObservation(delta);
131999
132937
  }
132000
132938
  appendAssistantDelta(delta) {
132001
132939
  if (this._streamingBlock === null) this.onStreamingTextStart();
132002
132940
  this._assistantDraft += delta;
132003
132941
  this.pendingAssistantFlush = true;
132942
+ this.recordSpeedObservation(delta);
132943
+ }
132944
+ /**
132945
+ * Feed an estimated tok/s observation into the shared speed gauge. Uses the
132946
+ * inter-delta elapsed time so a burst of small deltas produces an accurate
132947
+ * instantaneous rate. The gauge's 3s rolling window smooths jitter.
132948
+ */
132949
+ recordSpeedObservation(delta) {
132950
+ const now = performance.now();
132951
+ const tokens = estimateTokens(delta);
132952
+ if (this.lastDeltaAt !== void 0) {
132953
+ const elapsedMs = now - this.lastDeltaAt;
132954
+ if (elapsedMs > 0) {
132955
+ const rate = tokens / elapsedMs * 1e3;
132956
+ getSharedSpeedTracker().observe(rate, now);
132957
+ }
132958
+ }
132959
+ this.lastDeltaAt = now;
132004
132960
  }
132005
132961
  hasThinkingDraft() {
132006
132962
  return this._thinkingDraft.length > 0;
@@ -132267,6 +133223,8 @@ var StreamingUIController = class {
132267
133223
  this._assistantDraft = "";
132268
133224
  this._streamingBlock = null;
132269
133225
  this._thinkingDraft = "";
133226
+ this.lastDeltaAt = void 0;
133227
+ getSharedSpeedTracker().reset();
132270
133228
  this.disposeActiveThinkingComponent();
132271
133229
  }
132272
133230
  resetToolUi() {
@@ -133984,7 +134942,7 @@ var TranscriptController = class TranscriptController {
133984
134942
  this.host.state.ui.requestRender();
133985
134943
  }
133986
134944
  showError(message) {
133987
- this.showStatus(`错误:${message}`, this.host.state.theme.colors.error);
134945
+ this.showStatus(`错误:${truncateErrorMessage(replaceTabs(message))}`, this.host.state.theme.colors.error);
133988
134946
  }
133989
134947
  showProgressSpinner(label) {
133990
134948
  const tint = (s) => chalk.hex(this.host.state.theme.colors.primary)(s);
@@ -134106,7 +135064,7 @@ function pgrep(pattern) {
134106
135064
  });
134107
135065
  }
134108
135066
  function escapeShell(pattern) {
134109
- return `'${pattern.replace(/'/g, "'\\''")}'`;
135067
+ return `'${pattern.replaceAll("'", "'\\''")}'`;
134110
135068
  }
134111
135069
  async function checkWindows() {
134112
135070
  const pm2Active = await checkPm2();
@@ -135476,7 +136434,7 @@ const BOLD_OPEN = "\x1B[1m";
135476
136434
  const BOLD_CLOSE = "\x1B[22m";
135477
136435
  function hexToAnsi(hex) {
135478
136436
  const v = parseInt(hex.slice(1), 16);
135479
- return `\x1b[38;2;${v >> 16 & 255};${v >> 8 & 255};${v & 255}m`;
136437
+ return `\u001B[38;2;${v >> 16 & 255};${v >> 8 & 255};${v & 255}m`;
135480
136438
  }
135481
136439
  function compilePalette(palette) {
135482
136440
  const lowOpen = hexToAnsi(palette.low);
@@ -135971,6 +136929,22 @@ function formatContextStatus(usage, tokens, maxTokens) {
135971
136929
  if (maxTokens && maxTokens > 0 && tokens !== void 0) return `上下文:${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`;
135972
136930
  return `上下文:${pct}`;
135973
136931
  }
136932
+ const CONTEXT_WARNING_PERCENT_THRESHOLD = 70;
136933
+ const CONTEXT_WARNING_TOKEN_THRESHOLD = 14e4;
136934
+ const CONTEXT_ERROR_PERCENT_THRESHOLD = 90;
136935
+ const CONTEXT_ERROR_TOKEN_THRESHOLD = 18e4;
136936
+ function reachesThreshold(percent, maxTokens, percentThreshold, tokenThreshold) {
136937
+ if (!Number.isFinite(percent) || percent <= 0) return false;
136938
+ if (maxTokens === void 0 || !Number.isFinite(maxTokens) || maxTokens <= 0) return percent >= percentThreshold;
136939
+ const tokenPercentThreshold = tokenThreshold / maxTokens * 100;
136940
+ return percent >= Math.min(percentThreshold, tokenPercentThreshold);
136941
+ }
136942
+ function pickContextColor(usage, maxTokens, colors) {
136943
+ const percent = safeUsage(usage) * 100;
136944
+ if (reachesThreshold(percent, maxTokens, CONTEXT_ERROR_PERCENT_THRESHOLD, CONTEXT_ERROR_TOKEN_THRESHOLD)) return colors.error;
136945
+ if (reachesThreshold(percent, maxTokens, CONTEXT_WARNING_PERCENT_THRESHOLD, CONTEXT_WARNING_TOKEN_THRESHOLD)) return colors.warning;
136946
+ return colors.textDim;
136947
+ }
135974
136948
  const BRAND_COLORS = [
135975
136949
  "#72A4E9",
135976
136950
  "#A78BFA",
@@ -136138,7 +137112,8 @@ var FooterComponent = class {
136138
137112
  else {
136139
137113
  const statusLine = buildStatusLine(state.streamingPhase, state.livePaneMode, state.streamingStartTime);
136140
137114
  const ccDot = state.ccConnectActive ? chalk.hex(colors.success)("●") : chalk.hex(colors.textDim)("●");
136141
- rightText = chalk.hex(colors.textDim)(ccDot + " " + formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens) + " " + statusLine);
137115
+ const contextColor = pickContextColor(state.contextUsage, state.maxContextTokens, colors);
137116
+ rightText = `${ccDot} ${chalk.hex(contextColor)(formatContextStatus(state.contextUsage, state.contextTokens, state.maxContextTokens))}${chalk.hex(colors.textDim)(` ${statusLine}`)}`;
136142
137117
  }
136143
137118
  const rightWidth = visibleWidth(rightText);
136144
137119
  const gap = 3;
@@ -136250,7 +137225,7 @@ function renderRow(todo, colors) {
136250
137225
  }
136251
137226
  function statusMarker(status, colors) {
136252
137227
  switch (status) {
136253
- case "in_progress": return chalk.hex(colors.primary).bold("");
137228
+ case "in_progress": return chalk.hex(colors.primary).bold("");
136254
137229
  case "done": return chalk.hex(colors.success)("✓");
136255
137230
  case "pending": return chalk.hex(colors.textDim)("○");
136256
137231
  }
@@ -137487,14 +138462,14 @@ function makeBlockStyles(colors) {
137487
138462
  }
137488
138463
  function renderDisplayBlock(block, s, colors) {
137489
138464
  switch (block.type) {
137490
- case "diff": return renderDiffLinesClustered(block.old_text, block.new_text, block.path, colors, {
138465
+ case "diff": return renderDiffLinesClustered(replaceTabs(block.old_text), replaceTabs(block.new_text), block.path, colors, {
137491
138466
  contextLines: 3,
137492
138467
  expandKeyHint: "ctrl+e to preview",
137493
138468
  maxLines: DIFF_SUMMARY_MAX_LINES
137494
138469
  });
137495
138470
  case "file_content": {
137496
138471
  const lang = block.language ?? langFromPath(block.path);
137497
- const allLines = highlightLines(block.content, lang);
138472
+ const allLines = highlightLines(replaceTabs(block.content), lang);
137498
138473
  const shown = allLines.slice(0, CONTENT_SUMMARY_MAX_LINES);
137499
138474
  const lines = [s.strong(block.path)];
137500
138475
  for (const [i, line] of shown.entries()) lines.push(s.gutter(String(i + 1).padStart(4) + " ") + line);
@@ -137506,11 +138481,12 @@ function renderDisplayBlock(block, s, colors) {
137506
138481
  const lines = [];
137507
138482
  if (block.cwd !== void 0 && block.cwd.length > 0) lines.push(s.dim(`cwd: ${block.cwd}`));
137508
138483
  if (block.danger !== void 0) lines.push(s.errorBold(`Dangerous: ${block.danger}`));
137509
- (block.command.length > 0 ? block.command.split("\n") : [""]).forEach((cmdLine, idx) => {
138484
+ const command = replaceTabs(block.command);
138485
+ (command.length > 0 ? command.split("\n") : [""]).forEach((cmdLine, idx) => {
137510
138486
  const prefix = idx === 0 ? s.accent("$") : s.dim("·");
137511
138487
  lines.push(`${prefix} ${s.strong(cmdLine)}`);
137512
138488
  });
137513
- if (block.description !== void 0 && block.description.length > 0) lines.push(` ${s.dim(block.description)}`);
138489
+ if (block.description !== void 0 && block.description.length > 0) lines.push(` ${s.dim(replaceTabs(block.description))}`);
137514
138490
  return lines;
137515
138491
  }
137516
138492
  case "file_op": {
@@ -137529,14 +138505,14 @@ function renderDisplayBlock(block, s, colors) {
137529
138505
  if (block.description !== void 0 && block.description.length > 0) lines.push(s.dim(truncateOneLine(block.description, 200)));
137530
138506
  return lines;
137531
138507
  }
137532
- case "brief": return block.text ? block.text.split("\n").map((line) => line.length > 0 ? s.strong(line) : "") : [];
138508
+ case "brief": return block.text ? replaceTabs(block.text).split("\n").map((line) => line.length > 0 ? s.strong(line) : "") : [];
137533
138509
  case "background_task": return [s.strong(`${block.status} ${block.kind} task ${block.task_id}: ${block.description}`)];
137534
138510
  case "todo": return block.items.map((item) => s.strong(`- [${item.status}] ${item.title}`));
137535
138511
  default: return [];
137536
138512
  }
137537
138513
  }
137538
138514
  function normalizeApprovalText(text) {
137539
- return text.replaceAll("\r\n", "\n").trim();
138515
+ return replaceTabs(text.replaceAll("\r\n", "\n").trim());
137540
138516
  }
137541
138517
  function isDuplicateBriefBlock(block, description) {
137542
138518
  if (block.type !== "brief" || block.text.trim().length === 0) return false;
@@ -139962,7 +140938,7 @@ const DIM_RGB = [
139962
140938
  85
139963
140939
  ];
139964
140940
  function fg(r, g, b) {
139965
- return `\x1b[38;2;${r};${g};${b}m`;
140941
+ return `\u001B[38;2;${r};${g};${b}m`;
139966
140942
  }
139967
140943
  const RESET = "\x1B[0m";
139968
140944
  const BOLD = "\x1B[1m";
@@ -140011,7 +140987,7 @@ function getTerminalSize() {
140011
140987
  }
140012
140988
  function visualWidth(s) {
140013
140989
  let w = 0;
140014
- for (const ch of s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")) w += /[一-鿿 -〿＀-￯]/.test(ch) ? 2 : 1;
140990
+ for (const ch of s.replaceAll(/\u001B\[[0-9;]*[a-zA-Z]/g, "")) w += /[一-鿿 -〿＀-￯]/.test(ch) ? 2 : 1;
140015
140991
  return w;
140016
140992
  }
140017
140993
  function centerPad(text, width) {
@@ -140297,7 +141273,9 @@ const PLATFORMS = [
140297
141273
  ];
140298
141274
  async function question(rl, prompt) {
140299
141275
  return new Promise((resolve) => {
140300
- rl.question(prompt, (answer) => resolve(answer.trim()));
141276
+ rl.question(prompt, (answer) => {
141277
+ resolve(answer.trim());
141278
+ });
140301
141279
  });
140302
141280
  }
140303
141281
  function checkCcConnect() {
@@ -140774,10 +141752,10 @@ async function runStreamJson(opts) {
140774
141752
  if (existing.length > 0) try {
140775
141753
  session = await harness.resumeSession({ id: sessionKey });
140776
141754
  log.info("stream-json: resumed session", { sessionId: session.id });
140777
- } catch (err) {
141755
+ } catch (error) {
140778
141756
  log.warn("stream-json: resume failed, repairing session", {
140779
141757
  sessionKey,
140780
- error: String(err)
141758
+ error: String(error)
140781
141759
  });
140782
141760
  await harness.deleteSession(sessionKey).catch(() => {});
140783
141761
  const orphanDir = existing[0]?.sessionDir;