@botiverse/kimi-code-sdk 0.19.2-botiverse.0 → 0.20.0-botiverse.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -795,7 +795,7 @@ const KIMI_ERROR_INFO = {
795
795
  title: "Turn exceeded max steps",
796
796
  retryable: false,
797
797
  public: true,
798
- action: "Increase loop_control.max_steps_per_turn or split the task."
798
+ action: "Increase loop_control.max_steps_per_turn in config.toml or split the task."
799
799
  },
800
800
  "provider.api_error": {
801
801
  title: "Provider API error",
@@ -51668,7 +51668,7 @@ function withTelemetryProperties(telemetry, defaults) {
51668
51668
  * Loop-local error helpers.
51669
51669
  */
51670
51670
  function createMaxStepsExceededError(maxSteps, message) {
51671
- return new KimiError(ErrorCodes.LOOP_MAX_STEPS_EXCEEDED, message ?? `Turn exceeded maxSteps=${maxSteps}`, { details: { maxSteps } });
51671
+ return new KimiError(ErrorCodes.LOOP_MAX_STEPS_EXCEEDED, message ?? `Turn exceeded maxSteps=${maxSteps}. If max_steps_per_turn is too small, raise it in config.toml (loop_control.max_steps_per_turn), or run "/update-config" to update it, then "/reload".`, { details: { maxSteps } });
51672
51672
  }
51673
51673
  function isMaxStepsExceededError(error) {
51674
51674
  return isKimiError(error) && error.code === ErrorCodes.LOOP_MAX_STEPS_EXCEEDED;
@@ -51698,6 +51698,37 @@ function timeoutOutcome(timeoutMs, outcome) {
51698
51698
  timeout = void 0;
51699
51699
  } });
51700
51700
  }
51701
+ /**
51702
+ * Like `timeoutOutcome`, but the timer can be restarted via `reset()` while the
51703
+ * returned promise stays the same — so a `Promise.race` that already captured it
51704
+ * observes the new deadline. Used to extend a task's timeout (e.g. when a
51705
+ * foreground command is detached to the background).
51706
+ */
51707
+ function resettableTimeoutOutcome(initialMs, outcome) {
51708
+ let timer;
51709
+ let resolvePromise;
51710
+ const promise = new Promise((resolve) => {
51711
+ resolvePromise = resolve;
51712
+ });
51713
+ const clear = () => {
51714
+ if (timer === void 0) return;
51715
+ clearTimeout(timer);
51716
+ timer = void 0;
51717
+ };
51718
+ const reset = (timeoutMs) => {
51719
+ clear();
51720
+ if (timeoutMs === void 0 || timeoutMs <= 0) return;
51721
+ timer = setTimeout(() => {
51722
+ timer = void 0;
51723
+ resolvePromise(outcome);
51724
+ }, timeoutMs);
51725
+ };
51726
+ reset(initialMs);
51727
+ return Object.assign(promise, {
51728
+ reset,
51729
+ clear
51730
+ });
51731
+ }
51701
51732
  //#endregion
51702
51733
  //#region ../agent-core/src/utils/xml-escape.ts
51703
51734
  /** Escape XML content — escapes both tag and attribute boundary chars (& < > ") */
@@ -51723,14 +51754,11 @@ function escapeXmlTags(input) {
51723
51754
  * Title: ...
51724
51755
  * Severity: ...
51725
51756
  * <body>
51726
- * <task-notification> (only when source_kind === 'background_task' and tail_output is non-empty)
51727
- * <truncated tail>
51728
- * </task-notification>
51757
+ * <children...>
51729
51758
  * </notification>
51730
51759
  *
51731
- * The opening-tag names (`<notification ` / `<task-notification>`) are
51732
- * load-bearing for the projector's `mergeAdjacentUserMessages` detector
51733
- * — rename requires updating the detector too.
51760
+ * The opening tag name (`<notification `) is load-bearing for notification
51761
+ * consumers that detect chat-history injections.
51734
51762
  *
51735
51763
  * `agent_id` is emitted only for background_task notifications whose
51736
51764
  * source task is an agent subagent — surfacing it structurally lets the
@@ -51749,33 +51777,15 @@ function renderNotificationXml(data) {
51749
51777
  const title = typeof data["title"] === "string" ? data["title"] : "";
51750
51778
  const severity = typeof data["severity"] === "string" ? data["severity"] : "";
51751
51779
  const body = typeof data["body"] === "string" ? data["body"] : "";
51780
+ const children = childBlocks(data["children"] ?? data["extraBlocks"]);
51752
51781
  const lines = [`<notification id="${id}" category="${category}" type="${type}" source_kind="${sourceKind}" source_id="${sourceId}"${agentId === void 0 ? "" : ` agent_id="${agentId}"`}>`];
51753
51782
  if (title.length > 0) lines.push(`Title: ${title}`);
51754
51783
  if (severity.length > 0) lines.push(`Severity: ${severity}`);
51755
51784
  if (body.length > 0) lines.push(body);
51756
- if (data["source_kind"] === "background_task") {
51757
- const tailRaw = typeof data["tail_output"] === "string" ? data["tail_output"] : "";
51758
- if (tailRaw.length > 0) {
51759
- const truncated = truncateTailOutput(tailRaw, 20, 3e3);
51760
- lines.push("<task-notification>");
51761
- lines.push(truncated);
51762
- lines.push("</task-notification>");
51763
- }
51764
- }
51785
+ lines.push(...children);
51765
51786
  lines.push("</notification>");
51766
51787
  return lines.join("\n");
51767
51788
  }
51768
- /**
51769
- * Truncate tail output to at most `maxLines` lines and `maxChars`
51770
- * characters. Takes the *last* N lines, then trims from the front if
51771
- * the character budget is exceeded.
51772
- */
51773
- function truncateTailOutput(raw, maxLines, maxChars) {
51774
- const allLines = raw.split("\n");
51775
- let result = (allLines.length > maxLines ? allLines.slice(-maxLines) : allLines).join("\n");
51776
- if (result.length > maxChars) result = result.slice(-maxChars);
51777
- return result;
51778
- }
51779
51789
  function stringAttr$1(value, fallback) {
51780
51790
  if (typeof value !== "string" || value.length === 0) return fallback;
51781
51791
  return escapeXmlAttr(value);
@@ -51786,6 +51796,11 @@ function optionalStringAttr(value) {
51786
51796
  if (typeof value !== "string" || value.length === 0) return void 0;
51787
51797
  return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
51788
51798
  }
51799
+ function childBlocks(value) {
51800
+ if (typeof value === "string" && value.length > 0) return [value];
51801
+ if (!Array.isArray(value)) return [];
51802
+ return value.filter((item) => typeof item === "string" && item.length > 0);
51803
+ }
51789
51804
  //#endregion
51790
51805
  //#region ../agent-core/src/utils/per-id-json-store.ts
51791
51806
  /**
@@ -52357,6 +52372,7 @@ function isBackgroundTaskTerminal(status) {
52357
52372
  * reads the persisted log when available.
52358
52373
  */
52359
52374
  const MAX_OUTPUT_BYTES$1 = 1024 * 1024;
52375
+ const NOTIFICATION_FALLBACK_PREVIEW_BYTES = 3e3;
52360
52376
  const SIGTERM_GRACE_MS$1 = 5e3;
52361
52377
  const USER_INTERRUPT_REASON$1 = "Interrupted by user";
52362
52378
  const _ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
@@ -52382,7 +52398,6 @@ function emptyOutputSnapshot() {
52382
52398
  preview: ""
52383
52399
  };
52384
52400
  }
52385
- const NOTIFICATION_TAIL_BYTES = 3e3;
52386
52401
  var BackgroundManager = class {
52387
52402
  agent;
52388
52403
  persistence;
@@ -52446,6 +52461,7 @@ var BackgroundManager = class {
52446
52461
  const entryOptions = {
52447
52462
  detached,
52448
52463
  timeoutMs: options.timeoutMs ?? task.timeoutMs,
52464
+ detachTimeoutMs: options.detachTimeoutMs,
52449
52465
  signal: detached ? void 0 : options.signal
52450
52466
  };
52451
52467
  this.assertCanRegister(detached);
@@ -52569,6 +52585,7 @@ var BackgroundManager = class {
52569
52585
  const foregroundRelease = entry.foregroundRelease;
52570
52586
  if (foregroundRelease === void 0) return this.toInfo(entry);
52571
52587
  entry.foregroundRelease = void 0;
52588
+ if (entry.options.detachTimeoutMs !== void 0) entry.timeoutHandle?.reset(entry.options.detachTimeoutMs);
52572
52589
  try {
52573
52590
  entry.task.onDetach?.();
52574
52591
  } catch {}
@@ -52578,6 +52595,11 @@ var BackgroundManager = class {
52578
52595
  foregroundRelease.resolve("detached");
52579
52596
  return this.toInfo(entry);
52580
52597
  }
52598
+ persistOutput(taskId) {
52599
+ const entry = this.tasks.get(taskId);
52600
+ if (entry === void 0) return;
52601
+ this.startOutputPersist(entry);
52602
+ }
52581
52603
  /** Stop a running task. SIGTERM → 5s grace → SIGKILL. */
52582
52604
  async stop(taskId, reason) {
52583
52605
  const entry = this.tasks.get(taskId);
@@ -52753,7 +52775,8 @@ var BackgroundManager = class {
52753
52775
  if (this.scheduledNotificationKeys.has(key)) return;
52754
52776
  if (this.deliveredNotificationKeys.has(key)) return;
52755
52777
  this.scheduledNotificationKeys.add(key);
52756
- const tailOutput = (await this.getOutputSnapshot(info.taskId, NOTIFICATION_TAIL_BYTES)).preview;
52778
+ let output = await this.getOutputSnapshot(info.taskId, 0);
52779
+ if (!output.fullOutputAvailable) output = await this.getOutputSnapshot(info.taskId, NOTIFICATION_FALLBACK_PREVIEW_BYTES);
52757
52780
  if (this.isTerminalNotificationSuppressed(info.taskId)) return void 0;
52758
52781
  const notification = {
52759
52782
  id: origin.notificationId,
@@ -52765,7 +52788,7 @@ var BackgroundManager = class {
52765
52788
  title: `Background ${info.kind} ${info.status}`,
52766
52789
  severity: info.status === "completed" ? "info" : "warning",
52767
52790
  body: buildBackgroundTaskNotificationBody(info),
52768
- tail_output: tailOutput
52791
+ children: backgroundTaskNotificationChildren(output)
52769
52792
  };
52770
52793
  return {
52771
52794
  content: [{
@@ -52817,7 +52840,8 @@ var BackgroundManager = class {
52817
52840
  stopReason: entry.abortController.signal.aborted ? void 0 : errorMessage$2(error)
52818
52841
  });
52819
52842
  });
52820
- const timeout = timeoutOutcome(entry.options.timeoutMs, { kind: "timeout" });
52843
+ const timeout = resettableTimeoutOutcome(entry.options.timeoutMs, { kind: "timeout" });
52844
+ entry.timeoutHandle = timeout;
52821
52845
  const outcome = await Promise.race([
52822
52846
  worker.then((settlement) => ({
52823
52847
  kind: "worker",
@@ -52829,7 +52853,10 @@ var BackgroundManager = class {
52829
52853
  request
52830
52854
  })),
52831
52855
  this.signalOutcome(entry)
52832
- ]).finally(() => timeout.clear());
52856
+ ]).finally(() => {
52857
+ timeout.clear();
52858
+ entry.timeoutHandle = void 0;
52859
+ });
52833
52860
  const settlement = await this.settlementForOutcome(entry, outcome, worker);
52834
52861
  await this.finalizeTask(entry, settlement);
52835
52862
  }
@@ -52898,6 +52925,26 @@ var BackgroundManager = class {
52898
52925
  return entry.task.toInfo(base);
52899
52926
  }
52900
52927
  };
52928
+ function backgroundTaskNotificationChildren(output) {
52929
+ if (output.fullOutputAvailable && output.outputPath !== void 0) return [renderOutputFileBlock(output.outputPath, output.outputSizeBytes)];
52930
+ if (output.preview.length === 0) return void 0;
52931
+ return [renderOutputPreviewBlock(output)];
52932
+ }
52933
+ function renderOutputFileBlock(outputPath, outputSizeBytes) {
52934
+ return [
52935
+ `<output-file path="${escapeXmlAttr(outputPath)}" bytes="${String(outputSizeBytes)}">`,
52936
+ `Read the output file to retrieve the result: ${escapeXml(outputPath)}`,
52937
+ "</output-file>"
52938
+ ].join("\n");
52939
+ }
52940
+ function renderOutputPreviewBlock(output) {
52941
+ return [
52942
+ `<output-preview bytes="${String(output.previewBytes)}" total_bytes="${String(output.outputSizeBytes)}" truncated="${String(output.truncated)}">`,
52943
+ output.truncated ? `Showing the last ${String(output.previewBytes)} bytes. No persisted full output is available.` : "No persisted full output is available; this preview is the currently buffered task output.",
52944
+ escapeXml(output.preview),
52945
+ "</output-preview>"
52946
+ ].join("\n");
52947
+ }
52901
52948
  function notificationKey(origin) {
52902
52949
  return `${origin.taskId}\0${origin.status}\0${origin.notificationId}`;
52903
52950
  }
@@ -61836,6 +61883,23 @@ var ContextMemory = class {
61836
61883
  origin
61837
61884
  });
61838
61885
  }
61886
+ /**
61887
+ * Inject a user-invisible message and immediately send it to the model by
61888
+ * launching/steering a turn. The content is used as-is (no wrapper tag), so
61889
+ * callers can pass raw tool-result-style text or wrap it themselves. The
61890
+ * message is skipped on replay / transcript (so the user never sees it) but
61891
+ * is included in the context sent to the model. Use this for events the
61892
+ * model must react to right away without surfacing a user-visible message.
61893
+ */
61894
+ injectAndNotify(content, origin) {
61895
+ this.agent.turn.steer([{
61896
+ type: "text",
61897
+ text: content
61898
+ }], origin ?? {
61899
+ kind: "injection",
61900
+ variant: "system_reminder"
61901
+ });
61902
+ }
61839
61903
  appendLocalCommandStdout(content) {
61840
61904
  const text = `<local-command-stdout>\n${content.trim()}\n</local-command-stdout>`;
61841
61905
  this.appendMessage({
@@ -61851,6 +61915,40 @@ var ContextMemory = class {
61851
61915
  }
61852
61916
  });
61853
61917
  }
61918
+ appendBashInput(command) {
61919
+ const text = `<bash-input>\n${escapeXml(command)}\n</bash-input>`;
61920
+ this.appendMessage({
61921
+ role: "user",
61922
+ content: [{
61923
+ type: "text",
61924
+ text
61925
+ }],
61926
+ toolCalls: [],
61927
+ origin: {
61928
+ kind: "shell_command",
61929
+ phase: "input"
61930
+ }
61931
+ });
61932
+ }
61933
+ appendBashOutput(stdout, stderr, isError) {
61934
+ const text = `<bash-stdout>${escapeXml(stdout)}</bash-stdout><bash-stderr>${escapeXml(stderr)}</bash-stderr>`;
61935
+ this.appendMessage({
61936
+ role: "user",
61937
+ content: [{
61938
+ type: "text",
61939
+ text
61940
+ }],
61941
+ toolCalls: [],
61942
+ origin: isError === true ? {
61943
+ kind: "shell_command",
61944
+ phase: "output",
61945
+ isError: true
61946
+ } : {
61947
+ kind: "shell_command",
61948
+ phase: "output"
61949
+ }
61950
+ });
61951
+ }
61854
61952
  popMatchedMessage(matcher) {
61855
61953
  const lastDeferred = this.deferredMessages.at(-1);
61856
61954
  const last = lastDeferred ?? this._history.at(-1);
@@ -63082,6 +63180,33 @@ var PermissionModeInjector = class extends DynamicInjector {
63082
63180
  };
63083
63181
  //#endregion
63084
63182
  //#region ../agent-core/src/agent/injection/plugin-session-start.ts
63183
+ /**
63184
+ * Renders the `<plugin_session_start>` reminder blocks for the currently enabled
63185
+ * plugin session starts. Returns `undefined` when there is nothing to render
63186
+ * (no session starts, no registry, or no resolvable skills).
63187
+ *
63188
+ * Shared by the turn-loop injector (which dedups against history) and the
63189
+ * explicit `/reload` flow (which force-appends a fresh reminder).
63190
+ */
63191
+ function renderPluginSessionStartReminder(input) {
63192
+ const { sessionStarts, registry, log } = input;
63193
+ if (sessionStarts.length === 0) return void 0;
63194
+ if (registry === void 0) return void 0;
63195
+ const blocks = [];
63196
+ for (const sessionStart of sessionStarts) {
63197
+ const skill = registry.getPluginSkill(sessionStart.pluginId, sessionStart.skillName);
63198
+ if (skill === void 0) {
63199
+ log?.warn("plugin sessionStart skill not found", {
63200
+ pluginId: sessionStart.pluginId,
63201
+ skillName: sessionStart.skillName
63202
+ });
63203
+ continue;
63204
+ }
63205
+ blocks.push(renderSessionStartBlock(sessionStart, skill, registry.renderSkillPrompt(skill, "")));
63206
+ }
63207
+ if (blocks.length === 0) return void 0;
63208
+ return blocks.join("\n");
63209
+ }
63085
63210
  var PluginSessionStartInjector = class extends DynamicInjector {
63086
63211
  injectionVariant = "plugin_session_start";
63087
63212
  async getInjection() {
@@ -63091,24 +63216,11 @@ var PluginSessionStartInjector = class extends DynamicInjector {
63091
63216
  this.injectedAt = replayedAt;
63092
63217
  return;
63093
63218
  }
63094
- const sessionStarts = this.agent.pluginSessionStarts ?? [];
63095
- if (sessionStarts.length === 0) return void 0;
63096
- const registry = this.agent.skills?.registry;
63097
- if (registry === void 0) return void 0;
63098
- const blocks = [];
63099
- for (const sessionStart of sessionStarts) {
63100
- const skill = registry.getPluginSkill(sessionStart.pluginId, sessionStart.skillName);
63101
- if (skill === void 0) {
63102
- this.agent.log.warn("plugin sessionStart skill not found", {
63103
- pluginId: sessionStart.pluginId,
63104
- skillName: sessionStart.skillName
63105
- });
63106
- continue;
63107
- }
63108
- blocks.push(renderSessionStartBlock(sessionStart, skill, registry.renderSkillPrompt(skill, "")));
63109
- }
63110
- if (blocks.length === 0) return void 0;
63111
- return blocks.join("\n");
63219
+ return renderPluginSessionStartReminder({
63220
+ sessionStarts: this.agent.pluginSessionStarts,
63221
+ registry: this.agent.skills?.registry,
63222
+ log: this.agent.log
63223
+ });
63112
63224
  }
63113
63225
  };
63114
63226
  function renderSessionStartBlock(sessionStart, skill, skillContent) {
@@ -69252,7 +69364,7 @@ function isRecord$2(value) {
69252
69364
  }
69253
69365
  //#endregion
69254
69366
  //#region ../agent-core/src/skill/builtin/custom-theme.md?raw
69255
- var custom_theme_default = "---\nname: custom-theme\ndescription: Create or edit a kimi-code custom color theme — a JSON file under the resolved KIMI_CODE_HOME data directory that recolors the TUI. Use when the user wants their own theme, asks for a specific palette or mood, or wants to tweak an existing custom theme's colors.\n---\n\n# Create a kimi-code custom theme (custom-theme)\n\nHelp the user design, write, and apply a custom color theme for the kimi-code TUI. A theme is a single JSON file; the TUI ships with `dark`, `light`, and `auto`, and any file the user adds becomes selectable alongside them.\n\n## Rules of engagement\n\n- **Never write a theme until the user has explicitly clarified what they want.** This skill may only run after the user has confirmed light vs dark, the style or mood, any specific colors they care about, and the intended filename. If any of these are missing, ask before creating files.\n- **Never assume the data directory is `~/.kimi-code`.** Always resolve `$KIMI_CODE_HOME` first with the Bash command below.\n- **Never edit a live theme file in place.** Always create a `.json.new` candidate, validate it, back up the old file, and then `mv` it into place.\n- **Never overwrite an existing theme without reading it first.** Read, back up, then overwrite only after the user confirms.\n\n## Where a theme lives\n\nThe kimi-code runtime resolves the data directory as `KIMI_CODE_HOME` first, falling back to `~/.kimi-code`. Theme files live inside the `themes/` subdirectory of that data directory.\n\nBefore doing anything, resolve the actual data root with Bash so you don't write to the wrong place. Check whether `KIMI_CODE_HOME` is set and fall back to `~/.kimi-code` when it is empty:\n\n```bash\necho \"$KIMI_CODE_HOME\"\necho \"$HOME/.kimi-code\"\n```\n\nUse the first line when it is non-empty; otherwise use the second line. In the rest of this skill, `<KIMI_CODE_HOME>` means that resolved data root — **never assume `~/.kimi-code`**. Theme files live at `<KIMI_CODE_HOME>/themes/<name>.json`. Create the `themes/` directory if it doesn't exist.\n\n## What a theme is\n\n- A theme lives at `<KIMI_CODE_HOME>/themes/<name>.json`.\n- **The filename is the theme name**: `ember.json` shows up in the `/theme` picker as `Custom: ember`.\n- Shape:\n\n ```json\n {\n \"name\": \"ember\",\n \"displayName\": \"Ember\",\n \"colors\": {\n \"primary\": \"#83A598\",\n \"accent\": \"#FE8019\"\n }\n }\n ```\n\n - `name` (required), `displayName` (optional), `base` (optional: `\"dark\"` default, or `\"light\"`), `colors` (each value a 6-digit hex `#RRGGBB`).\n- **Partial themes are fine**: any token you leave out falls back to the **base** palette (`dark` by default; set `\"base\": \"light\"` for a light theme), so you can recolor just a few tokens or all of them.\n\n## Source of truth: the docs token reference\n\nBefore choosing colors, use **FetchURL** to fetch the official custom-theme docs as the authoritative list of tokens and what each controls:\n\n```\nhttps://moonshotai.github.io/kimi-code/en/customization/themes.html\n```\n\nOnly set tokens from this set — unknown keys are silently ignored at load. If FetchURL is unavailable or the fetch fails, fall back to the embedded reference below (it mirrors the same tokens) and tell the user you're working from the built-in list rather than the live docs.\n\n## Color tokens (what each controls)\n\n| Token | Controls |\n| --- | --- |\n| `primary` | The most-used color: links, inline code, the selected item in nearly every dialog, the focused editor border, plan/\"running\" badges, spinners |\n| `accent` | Secondary highlight: approval `▶` prefix, device-code box, image placeholder, BTW / queue panes, registry import |\n| `text` | Body text: dialog bodies, todo titles, footer model label, Markdown headings, assistant/tool message bullets, list bullets |\n| `textStrong` | Emphasized / bold text: input dialogs, status messages |\n| `textDim` | Secondary, dimmed text (the most widely used dim shade): thinking, hints, descriptions, completed todos, Markdown quotes, footer status bar |\n| `textMuted` | Faintest text: counters, scroll info, descriptions, Markdown link URLs, code-block borders |\n| `border` | Pane and editor borders, Markdown horizontal rule |\n| `borderFocus` | Focus / attention border (currently only the approval panel) |\n| `success` | Success state: `✓`, \"enabled\", completed |\n| `warning` | Warning state: auto/yolo badges, stale markers, plan-mode hint |\n| `error` | Error state: error messages, failed tool output |\n| `diffAdded` | Diff added lines |\n| `diffRemoved` | Diff removed lines |\n| `diffAddedStrong` | Diff intra-line changed words, added (bold) |\n| `diffRemovedStrong` | Diff intra-line changed words, removed (bold) |\n| `diffGutter` | Diff line-number gutter |\n| `diffMeta` | Diff meta / hunk headers |\n| `roleUser` | User message bullet and text, skill-activation name (the one role color with its own hue) |\n\n## Workflow\n\n1. **Ask the user what they want first — before choosing any colors.** Clarify, in one short exchange:\n - **Light or dark?** A light theme (dark text on a light background) or a dark theme (light text on a dark background). This sets the whole direction, so settle it first. For a light theme, set `\"base\": \"light\"` so the tokens you leave out inherit the light palette instead of dark.\n - **What style / mood?** e.g. warm vs cool, vivid vs muted, high vs low contrast, a named vibe (\"nord\", \"solarized\", \"sunset\"), or a base to start from (an existing theme, or `dark` / `light`).\n - **Any specific colors?** Whether they have exact hex values to anchor on (a brand color, a preferred `primary`, etc.).\n\n For the discrete choices (light vs dark, a few style options), prefer **AskUserQuestion** if it is available. If you are running in **auto mode** and `AskUserQuestion` is unavailable, ask the same question as a plain-text message with clear numbered or bulleted options, and wait for the user's reply. Don't start picking colors until you at least know light-vs-dark and the rough style.\n\n2. **Resolve the actual theme directory and current theme(s).**\n - Resolve the data root by checking `echo \"$KIMI_CODE_HOME\"`; if empty, use `echo \"$HOME/.kimi-code\"`. Use `<root>/themes` for every subsequent step.\n - If tweaking an existing custom theme, **Read** `<KIMI_CODE_HOME>/themes/<name>.json` first — never overwrite a theme you haven't read.\n - Starting fresh: build a `colors` object from the token table. You can `ls <KIMI_CODE_HOME>/themes/` and Read one of the user's existing themes as a reference for the format.\n\n3. **Pick a starting point and choose colors deliberately.**\n - Every value is a 6-digit hex `#RRGGBB` (not 3-digit, not a named color).\n - Keep contrast usable against the user's terminal background: don't let `text` / `textDim` sit too close to the background, and keep `success` / `warning` / `error` clearly distinguishable from each other.\n - `primary` is the most-seen color (links, selection, focus) — make it readable and distinct from `text`.\n - `roleUser` is the one role color meant to stand on its own — give it a distinct hue.\n\n4. **Create a candidate file; never edit the live theme in place.**\n - Use Bash to create a candidate. If the target theme already exists, copy it verbatim: `cp <name>.json <name>.json.new` (inside `<KIMI_CODE_HOME>/themes/`). If it doesn't exist, use **Write** to create a minimal skeleton named `<name>.json.new`.\n - Use **Edit** on the candidate to change only the intended keys. Keep every existing entry, comment, and formatting intact.\n\n5. **Validate the candidate before overwriting.**\n - Read the candidate with **Read** to visually confirm it is well-formed JSON and that every `colors` value is a full 6-digit hex `#RRGGBB` (not 3-digit, not a named color).\n - Invalid hex values are silently skipped at load (they fall back to the base palette), but fix them so the theme renders as intended.\n\n6. **Back up and overwrite.**\n - Back up the old file first — **always** create a new timestamped backup and never overwrite an existing backup: `cp <name>.json \"<name>.json.$(date +%Y%m%d-%H%M%S).bak\"`.\n - If the target didn't exist, skip the backup.\n - Overwrite with the candidate: `mv <name>.json.new <name>.json`.\n\n7. **Tell the user how to apply it** (next section).\n\n## Applying the theme\n\n- The `/theme` picker re-scans the themes directory every time it opens, so a newly added file shows up **without restarting** — tell the user to run `/theme` and choose `Custom: <name>`.\n- Or set it in `tui.toml`: `theme = \"<name>\"`.\n- **Editing the active theme**: changes to the theme that's *currently in use* are not auto-reloaded. Tell the user to run **`/reload-tui`** (or switch to another theme and back). Re-selecting the **same** theme in `/theme` is a no-op (\"Theme unchanged\").\n\n## Don'ts\n\n- **Don't start creating or editing a theme until the user has clarified light/dark, style/mood, any specific colors, and the filename.** If anything is unclear, ask — don't guess.\n- Don't invent token names — only use the documented set; unknown keys are silently ignored.\n- Don't write 3-digit hex or named colors — use full `#RRGGBB`.\n- Never edit the live theme file in place; work through a candidate and validate before `mv`.\n- Before overwriting an existing theme file, **read it and back it up** so the user can recover.\n- Don't tell the user to restart the app to apply a theme — `/theme` or `/reload-tui` is enough.\n";
69367
+ var custom_theme_default = "---\nname: custom-theme\ndescription: Create or edit a kimi-code custom color theme — a JSON file under the resolved KIMI_CODE_HOME data directory that recolors the TUI. Use when the user wants their own theme, asks for a specific palette or mood, or wants to tweak an existing custom theme's colors.\n---\n\n# Create a kimi-code custom theme (custom-theme)\n\nHelp the user design, write, and apply a custom color theme for the kimi-code TUI. A theme is a single JSON file; the TUI ships with `dark`, `light`, and `auto`, and any file the user adds becomes selectable alongside them.\n\n## Rules of engagement\n\n- **Never write a theme until the user has explicitly clarified what they want.** This skill may only run after the user has confirmed light vs dark, the style or mood, any specific colors they care about, and the intended filename. If any of these are missing, ask before creating files.\n- **Never assume the data directory is `~/.kimi-code`.** Always resolve `$KIMI_CODE_HOME` first with the Bash command below.\n- **Never edit a live theme file in place.** Always create a `.json.new` candidate, validate it, back up the old file, and then `mv` it into place.\n- **Never overwrite an existing theme without reading it first.** Read, back up, then overwrite only after the user confirms.\n\n## Where a theme lives\n\nThe kimi-code runtime resolves the data directory as `KIMI_CODE_HOME` first, falling back to `~/.kimi-code`. Theme files live inside the `themes/` subdirectory of that data directory.\n\nBefore doing anything, resolve the actual data root with Bash so you don't write to the wrong place. Check whether `KIMI_CODE_HOME` is set and fall back to `~/.kimi-code` when it is empty:\n\n```bash\necho \"$KIMI_CODE_HOME\"\necho \"$HOME/.kimi-code\"\n```\n\nUse the first line when it is non-empty; otherwise use the second line. In the rest of this skill, `<KIMI_CODE_HOME>` means that resolved data root — **never assume `~/.kimi-code`**. Theme files live at `<KIMI_CODE_HOME>/themes/<name>.json`. Create the `themes/` directory if it doesn't exist.\n\n## What a theme is\n\n- A theme lives at `<KIMI_CODE_HOME>/themes/<name>.json`.\n- **The filename is the theme name**: `ember.json` shows up in the `/theme` picker as `Custom: ember`.\n- Shape:\n\n ```json\n {\n \"name\": \"ember\",\n \"displayName\": \"Ember\",\n \"colors\": {\n \"primary\": \"#83A598\",\n \"accent\": \"#FE8019\"\n }\n }\n ```\n\n - `name` (required), `displayName` (optional), `base` (optional: `\"dark\"` default, or `\"light\"`), `colors` (each value a 6-digit hex `#RRGGBB`).\n- **Partial themes are fine**: any token you leave out falls back to the **base** palette (`dark` by default; set `\"base\": \"light\"` for a light theme), so you can recolor just a few tokens or all of them.\n\n## Source of truth: the docs token reference\n\nBefore choosing colors, use **FetchURL** to fetch the official custom-theme docs as the authoritative list of tokens and what each controls:\n\n```\nhttps://moonshotai.github.io/kimi-code/en/customization/themes.html\n```\n\nOnly set tokens from this set — unknown keys are silently ignored at load. If FetchURL is unavailable or the fetch fails, fall back to the embedded reference below (it mirrors the same tokens) and tell the user you're working from the built-in list rather than the live docs.\n\n## Color tokens (what each controls)\n\n| Token | Controls |\n| --- | --- |\n| `primary` | The most-used color: links, inline code, the selected item in nearly every dialog, the focused editor border, plan/\"running\" badges, spinners |\n| `accent` | Secondary highlight: approval `▶` prefix, device-code box, image placeholder, BTW / queue panes, registry import |\n| `text` | Body text: dialog bodies, todo titles, footer model label, Markdown headings, assistant/tool message bullets, list bullets |\n| `textStrong` | Emphasized / bold text: input dialogs, status messages |\n| `textDim` | Secondary, dimmed text (the most widely used dim shade): thinking, hints, descriptions, completed todos, Markdown quotes, footer status bar |\n| `textMuted` | Faintest text: counters, scroll info, descriptions, Markdown link URLs, code-block borders |\n| `border` | Pane and editor borders, Markdown horizontal rule |\n| `borderFocus` | Focus / attention border (currently only the approval panel) |\n| `success` | Success state: `✓`, \"enabled\", completed |\n| `warning` | Warning state: auto/yolo badges, stale markers, plan-mode hint |\n| `error` | Error state: error messages, failed tool output |\n| `diffAdded` | Diff added lines |\n| `diffRemoved` | Diff removed lines |\n| `diffAddedStrong` | Diff intra-line changed words, added (bold) |\n| `diffRemovedStrong` | Diff intra-line changed words, removed (bold) |\n| `diffGutter` | Diff line-number gutter |\n| `diffMeta` | Diff meta / hunk headers |\n| `roleUser` | User message bullet and text, skill-activation name (the one role color with its own hue) |\n| `shellMode` | Shell mode (`!`) prompt, editor border, and the echoed `$ command` line |\n\n## Workflow\n\n1. **Ask the user what they want first — before choosing any colors.** Clarify, in one short exchange:\n - **Light or dark?** A light theme (dark text on a light background) or a dark theme (light text on a dark background). This sets the whole direction, so settle it first. For a light theme, set `\"base\": \"light\"` so the tokens you leave out inherit the light palette instead of dark.\n - **What style / mood?** e.g. warm vs cool, vivid vs muted, high vs low contrast, a named vibe (\"nord\", \"solarized\", \"sunset\"), or a base to start from (an existing theme, or `dark` / `light`).\n - **Any specific colors?** Whether they have exact hex values to anchor on (a brand color, a preferred `primary`, etc.).\n\n For the discrete choices (light vs dark, a few style options), prefer **AskUserQuestion** if it is available. If you are running in **auto mode** and `AskUserQuestion` is unavailable, ask the same question as a plain-text message with clear numbered or bulleted options, and wait for the user's reply. Don't start picking colors until you at least know light-vs-dark and the rough style.\n\n2. **Resolve the actual theme directory and current theme(s).**\n - Resolve the data root by checking `echo \"$KIMI_CODE_HOME\"`; if empty, use `echo \"$HOME/.kimi-code\"`. Use `<root>/themes` for every subsequent step.\n - If tweaking an existing custom theme, **Read** `<KIMI_CODE_HOME>/themes/<name>.json` first — never overwrite a theme you haven't read.\n - Starting fresh: build a `colors` object from the token table. You can `ls <KIMI_CODE_HOME>/themes/` and Read one of the user's existing themes as a reference for the format.\n\n3. **Pick a starting point and choose colors deliberately.**\n - Every value is a 6-digit hex `#RRGGBB` (not 3-digit, not a named color).\n - Keep contrast usable against the user's terminal background: don't let `text` / `textDim` sit too close to the background, and keep `success` / `warning` / `error` clearly distinguishable from each other.\n - `primary` is the most-seen color (links, selection, focus) — make it readable and distinct from `text`.\n - `roleUser` is the one role color meant to stand on its own — give it a distinct hue.\n\n4. **Create a candidate file; never edit the live theme in place.**\n - Use Bash to create a candidate. If the target theme already exists, copy it verbatim: `cp <name>.json <name>.json.new` (inside `<KIMI_CODE_HOME>/themes/`). If it doesn't exist, use **Write** to create a minimal skeleton named `<name>.json.new`.\n - Use **Edit** on the candidate to change only the intended keys. Keep every existing entry, comment, and formatting intact.\n\n5. **Validate the candidate before overwriting.**\n - Read the candidate with **Read** to visually confirm it is well-formed JSON and that every `colors` value is a full 6-digit hex `#RRGGBB` (not 3-digit, not a named color).\n - Invalid hex values are silently skipped at load (they fall back to the base palette), but fix them so the theme renders as intended.\n\n6. **Back up and overwrite.**\n - Back up the old file first — **always** create a new timestamped backup and never overwrite an existing backup: `cp <name>.json \"<name>.json.$(date +%Y%m%d-%H%M%S).bak\"`.\n - If the target didn't exist, skip the backup.\n - Overwrite with the candidate: `mv <name>.json.new <name>.json`.\n\n7. **Tell the user how to apply it** (next section).\n\n## Applying the theme\n\n- The `/theme` picker re-scans the themes directory every time it opens, so a newly added file shows up **without restarting** — tell the user to run `/theme` and choose `Custom: <name>`.\n- Or set it in `tui.toml`: `theme = \"<name>\"`.\n- **Editing the active theme**: changes to the theme that's *currently in use* are not auto-reloaded. Tell the user to run **`/reload-tui`** (or switch to another theme and back). Re-selecting the **same** theme in `/theme` is a no-op (\"Theme unchanged\").\n\n## Don'ts\n\n- **Don't start creating or editing a theme until the user has clarified light/dark, style/mood, any specific colors, and the filename.** If anything is unclear, ask — don't guess.\n- Don't invent token names — only use the documented set; unknown keys are silently ignored.\n- Don't write 3-digit hex or named colors — use full `#RRGGBB`.\n- Never edit the live theme file in place; work through a candidate and validate before `mv`.\n- Before overwriting an existing theme file, **read it and back it up** so the user can recover.\n- Don't tell the user to restart the app to apply a theme — `/theme` or `/reload-tui` is enough.\n";
69256
69368
  //#endregion
69257
69369
  //#region ../agent-core/src/skill/builtin/custom-theme.ts
69258
69370
  const PSEUDO_PATH$4 = "builtin://custom-theme";
@@ -69796,8 +69908,17 @@ function formatModelSkill(skill) {
69796
69908
  lines.push(` Path: ${skill.path}`);
69797
69909
  return lines;
69798
69910
  }
69911
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
69799
69912
  function truncate(value, max) {
69800
- return value.length > max ? value.slice(0, max) : value;
69913
+ if (value.length <= max) return value;
69914
+ let length = 0;
69915
+ let result = "";
69916
+ for (const { segment } of graphemeSegmenter.segment(value)) {
69917
+ if (length + segment.length > max - 1) break;
69918
+ result += segment;
69919
+ length += segment.length;
69920
+ }
69921
+ return `${result}…`;
69801
69922
  }
69802
69923
  //#endregion
69803
69924
  //#region ../agent-core/src/agent/skill/prompt.ts
@@ -77806,9 +77927,17 @@ function normalizeToolResult(r) {
77806
77927
  const textJoined = r.output.filter((c) => c.type === "text").map((c) => c.text).join("");
77807
77928
  output = textJoined.length > 0 ? textJoined : TOOL_OUTPUT_EMPTY;
77808
77929
  }
77809
- return r.isError === true ? {
77930
+ if (r.isError === true) return r.truncated === true ? {
77931
+ output,
77932
+ isError: true,
77933
+ truncated: true
77934
+ } : {
77810
77935
  output,
77811
77936
  isError: true
77937
+ };
77938
+ return r.truncated === true ? {
77939
+ output,
77940
+ truncated: true
77812
77941
  } : { output };
77813
77942
  }
77814
77943
  function makeToolResult(call, args, result) {
@@ -78891,6 +79020,11 @@ const injectionOriginSchema = z.object({
78891
79020
  kind: z.literal("injection"),
78892
79021
  variant: z.string()
78893
79022
  });
79023
+ const shellCommandOriginSchema = z.object({
79024
+ kind: z.literal("shell_command"),
79025
+ phase: z.enum(["input", "output"]),
79026
+ isError: z.boolean().optional()
79027
+ });
78894
79028
  const compactionSummaryOriginSchema = z.object({ kind: z.literal("compaction_summary") });
78895
79029
  const systemTriggerOriginSchema = z.object({
78896
79030
  kind: z.literal("system_trigger"),
@@ -78935,6 +79069,7 @@ const promptOriginSchema = z.discriminatedUnion("kind", [
78935
79069
  userPromptOriginSchema,
78936
79070
  skillActivationOriginSchema,
78937
79071
  injectionOriginSchema,
79072
+ shellCommandOriginSchema,
78938
79073
  compactionSummaryOriginSchema,
78939
79074
  systemTriggerOriginSchema,
78940
79075
  backgroundTaskOriginSchema,
@@ -79282,6 +79417,16 @@ const toolProgressEventSchema = z.object({
79282
79417
  toolCallId: z.string(),
79283
79418
  update: toolUpdateSchema
79284
79419
  });
79420
+ const shellOutputEventSchema = z.object({
79421
+ type: z.literal("shell.output"),
79422
+ commandId: z.string(),
79423
+ update: toolUpdateSchema
79424
+ });
79425
+ const shellStartedEventSchema = z.object({
79426
+ type: z.literal("shell.started"),
79427
+ commandId: z.string(),
79428
+ taskId: z.string()
79429
+ });
79285
79430
  const toolResultEventSchema = z.object({
79286
79431
  type: z.literal("tool.result"),
79287
79432
  turnId: z.number(),
@@ -79408,6 +79553,8 @@ const eventSchema = z.discriminatedUnion("type", [
79408
79553
  toolCallDeltaEventSchema,
79409
79554
  toolCallStartedEventSchema,
79410
79555
  toolProgressEventSchema,
79556
+ shellOutputEventSchema,
79557
+ shellStartedEventSchema,
79411
79558
  toolResultEventSchema,
79412
79559
  toolListUpdatedEventSchema,
79413
79560
  mcpServerStatusEventSchema,
@@ -79879,8 +80026,7 @@ const questionRequestSchema = z.object({
79879
80026
  turn_id: z.number().int().nonnegative().optional(),
79880
80027
  tool_call_id: z.string().min(1).optional(),
79881
80028
  questions: z.array(questionItemSchema).min(1).max(4),
79882
- created_at: isoDateTimeSchema,
79883
- expires_at: isoDateTimeSchema
80029
+ created_at: isoDateTimeSchema
79884
80030
  });
79885
80031
  const questionAnswerSchema = z.discriminatedUnion("kind", [
79886
80032
  z.object({
@@ -80460,6 +80606,16 @@ const sessionStatusResponseSchema = z.object({
80460
80606
  max_context_tokens: z.number().int().nonnegative(),
80461
80607
  context_usage: z.number().min(0).max(1)
80462
80608
  });
80609
+ const sessionWarningSchema = z.object({
80610
+ code: z.string(),
80611
+ message: z.string(),
80612
+ severity: z.enum([
80613
+ "info",
80614
+ "warning",
80615
+ "error"
80616
+ ])
80617
+ });
80618
+ z.object({ warnings: z.array(sessionWarningSchema) });
80463
80619
  z.preprocess((value) => value === void 0 ? {} : value, z.object({ instruction: z.string().optional() }));
80464
80620
  z.object({});
80465
80621
  z.preprocess((value) => value === void 0 ? {} : value, z.object({
@@ -84393,8 +84549,14 @@ function mcpResultToExecutableOutput(result, qualifiedToolName) {
84393
84549
  const part = convertMCPContentBlock(block);
84394
84550
  if (part !== null) converted.push(part);
84395
84551
  }
84396
- return {
84397
- output: collapseSingleText(applyOutputLimits(wrapMediaOnly(converted, qualifiedToolName))),
84552
+ const limited = applyOutputLimits(wrapMediaOnly(converted, qualifiedToolName));
84553
+ const output = collapseSingleText(limited.parts);
84554
+ return limited.truncated ? {
84555
+ output,
84556
+ isError: result.isError,
84557
+ truncated: true
84558
+ } : {
84559
+ output,
84398
84560
  isError: result.isError
84399
84561
  };
84400
84562
  }
@@ -84428,11 +84590,13 @@ function wrapMediaOnly(parts, qualifiedToolName) {
84428
84590
  */
84429
84591
  function applyOutputLimits(parts) {
84430
84592
  let remaining = MCP_MAX_OUTPUT_CHARS;
84593
+ let truncated = false;
84431
84594
  let textTruncated = false;
84432
84595
  const out = [];
84433
84596
  for (const part of parts) {
84434
84597
  if (part.type === "text") {
84435
84598
  if (remaining <= 0) {
84599
+ truncated = true;
84436
84600
  textTruncated = true;
84437
84601
  continue;
84438
84602
  }
@@ -84442,6 +84606,7 @@ function applyOutputLimits(parts) {
84442
84606
  text: part.text.slice(0, remaining)
84443
84607
  });
84444
84608
  remaining = 0;
84609
+ truncated = true;
84445
84610
  textTruncated = true;
84446
84611
  } else {
84447
84612
  out.push(part);
@@ -84452,6 +84617,7 @@ function applyOutputLimits(parts) {
84452
84617
  if (part.type === "think") {
84453
84618
  const size = part.think.length + (part.encrypted?.length ?? 0);
84454
84619
  if (remaining <= 0) {
84620
+ truncated = true;
84455
84621
  textTruncated = true;
84456
84622
  continue;
84457
84623
  }
@@ -84461,6 +84627,7 @@ function applyOutputLimits(parts) {
84461
84627
  think: part.think.slice(0, remaining)
84462
84628
  });
84463
84629
  remaining = 0;
84630
+ truncated = true;
84464
84631
  textTruncated = true;
84465
84632
  } else {
84466
84633
  out.push(part);
@@ -84475,12 +84642,16 @@ function applyOutputLimits(parts) {
84475
84642
  type: "text",
84476
84643
  text: binaryPartTooLargeNotice(kind, url.length)
84477
84644
  });
84645
+ truncated = true;
84478
84646
  continue;
84479
84647
  }
84480
84648
  out.push(part);
84481
84649
  }
84482
84650
  if (textTruncated) appendTruncationNotice(out);
84483
- return out;
84651
+ return {
84652
+ parts: out,
84653
+ truncated
84654
+ };
84484
84655
  }
84485
84656
  function appendTruncationNotice(out) {
84486
84657
  for (let i = out.length - 1; i >= 0; i--) {
@@ -84591,25 +84762,25 @@ async function listDirectory(kaos, workDir = kaos.getcwd(), options = {}) {
84591
84762
  }
84592
84763
  //#endregion
84593
84764
  //#region ../agent-core/src/profile/context.ts
84594
- const AGENTS_MD_MAX_BYTES = 32 * 1024;
84595
- const AGENTS_MD_TRUNCATION_MARKER = "<!-- Some AGENTS.md files were truncated or omitted to fit the 32 KB budget -->";
84765
+ const AGENTS_MD_RECOMMENDED_MAX_BYTES = 32 * 1024;
84596
84766
  const S_IFMT$3 = 61440;
84597
84767
  const S_IFREG$1 = 32768;
84598
84768
  async function prepareSystemPromptContext(kaos, brandHome, options) {
84599
84769
  const additionalDirs = normalizeAdditionalDirs(options?.additionalDirs ?? []);
84600
- const [cwdListing, agentsMd, additionalDirsInfo] = await Promise.all([
84770
+ const [cwdListing, agentsMdResult, additionalDirsInfo] = await Promise.all([
84601
84771
  listDirectory(kaos, void 0, { collapseHiddenDirs: true }),
84602
- loadAgentsMd(kaos, brandHome),
84772
+ loadAgentsMdForRoots(kaos, brandHome, [kaos.getcwd()]),
84603
84773
  loadAdditionalDirsInfo(kaos, additionalDirs)
84604
84774
  ]);
84605
84775
  return {
84606
84776
  cwdListing,
84607
- agentsMd,
84608
- additionalDirsInfo
84777
+ agentsMd: agentsMdResult.content,
84778
+ additionalDirsInfo,
84779
+ agentsMdWarning: agentsMdResult.warning
84609
84780
  };
84610
84781
  }
84611
84782
  async function loadAgentsMd(kaos, brandHome) {
84612
- return loadAgentsMdForRoots(kaos, brandHome, [kaos.getcwd()]);
84783
+ return (await loadAgentsMdForRoots(kaos, brandHome, [kaos.getcwd()])).content;
84613
84784
  }
84614
84785
  async function loadAgentsMdForRoots(kaos, brandHome, workDirs) {
84615
84786
  const discovered = [];
@@ -84636,7 +84807,12 @@ async function loadAgentsMdForRoots(kaos, brandHome, workDirs) {
84636
84807
  for (const fileName of ["AGENTS.md", "agents.md"]) if (await collect(join$1(dir, fileName))) break;
84637
84808
  }
84638
84809
  }
84639
- return renderAgentFiles(discovered);
84810
+ const content = renderAgentFiles(discovered);
84811
+ const totalBytes = byteLength(content);
84812
+ return {
84813
+ content,
84814
+ warning: totalBytes > AGENTS_MD_RECOMMENDED_MAX_BYTES ? `AGENTS.md total ${formatKB(totalBytes)} KB exceeds the recommended ${formatKB(AGENTS_MD_RECOMMENDED_MAX_BYTES)} KB. Large instruction files increase cost and may impact performance; consider trimming.` : void 0
84815
+ };
84640
84816
  }
84641
84817
  async function loadAdditionalDirsInfo(kaos, additionalDirs) {
84642
84818
  return (await Promise.all(additionalDirs.map(async (dir) => {
@@ -84691,60 +84867,15 @@ async function isFile$2(kaos, path) {
84691
84867
  }
84692
84868
  function renderAgentFiles(files) {
84693
84869
  if (files.length === 0) return "";
84694
- let remaining = AGENTS_MD_MAX_BYTES;
84695
- let didTruncate = false;
84696
- const budgeted = Array.from({ length: files.length });
84697
- for (let i = files.length - 1; i >= 0; i--) {
84698
- const file = files[i];
84699
- if (file === void 0) continue;
84700
- const annotation = annotationFor(file.path);
84701
- const separator = i < files.length - 1 ? "\n\n" : "";
84702
- remaining -= byteLength(annotation) + byteLength(separator);
84703
- if (remaining <= 0) {
84704
- budgeted[i] = {
84705
- path: file.path,
84706
- content: ""
84707
- };
84708
- remaining = 0;
84709
- didTruncate = true;
84710
- continue;
84711
- }
84712
- let content = file.content;
84713
- if (byteLength(content) > remaining) {
84714
- content = truncateUtf8(content, remaining).trim();
84715
- didTruncate = true;
84716
- }
84717
- remaining -= byteLength(content);
84718
- budgeted[i] = {
84719
- path: file.path,
84720
- content
84721
- };
84722
- }
84723
- const rendered = budgeted.filter((file) => file !== void 0 && file.content.length > 0).map((file) => `${annotationFor(file.path)}${file.content}`).join("\n\n");
84724
- return didTruncate ? `${AGENTS_MD_TRUNCATION_MARKER}\n${rendered}` : rendered;
84725
- }
84726
- function truncateUtf8(text, maxBytes) {
84727
- if (maxBytes <= 0) return "";
84728
- if (byteLength(text) <= maxBytes) return text;
84729
- let low = 0;
84730
- let high = text.length;
84731
- while (low < high) {
84732
- const mid = Math.ceil((low + high) / 2);
84733
- if (byteLength(text.slice(0, mid)) <= maxBytes) low = mid;
84734
- else high = mid - 1;
84735
- }
84736
- let result = text.slice(0, low);
84737
- while (endsWithUnpairedHighSurrogate(result)) result = result.slice(0, -1);
84738
- return result;
84739
- }
84740
- function endsWithUnpairedHighSurrogate(text) {
84741
- if (text.length === 0) return false;
84742
- const codePoint = text.codePointAt(text.length - 1);
84743
- return codePoint !== void 0 && codePoint >= 55296 && codePoint <= 56319;
84870
+ return files.map((file) => `${annotationFor(file.path)}${file.content}`).join("\n\n");
84744
84871
  }
84745
84872
  function byteLength(text) {
84746
84873
  return Buffer.byteLength(text, "utf8");
84747
84874
  }
84875
+ function formatKB(bytes) {
84876
+ const kb = bytes / 1024;
84877
+ return Number.isInteger(kb) ? String(kb) : kb.toFixed(1);
84878
+ }
84748
84879
  function annotationFor(path) {
84749
84880
  return `<!-- From: ${path} -->\n`;
84750
84881
  }
@@ -85622,21 +85753,44 @@ async function disposeProcess$2(proc) {
85622
85753
  * directory is not a git repository or no useful information was collected.
85623
85754
  */
85624
85755
  async function collectGitContext(kaos, cwd) {
85625
- if (await runGit(kaos, cwd, ["rev-parse", "--is-inside-work-tree"]) === null) return "";
85626
- const [remoteUrl, branch, dirtyRaw, logRaw] = await Promise.all([
85627
- runGit(kaos, cwd, [
85756
+ const revParseArgs = ["rev-parse", "--is-inside-work-tree"];
85757
+ const revParse = await runGit(kaos, cwd, revParseArgs);
85758
+ if (!revParse.ok) {
85759
+ if (revParse.kind === "command-failed" && isNotARepo(revParse.stderr)) return `<git-context status="unavailable" reason="not-a-repo"/>`;
85760
+ logGitFailure(cwd, revParseArgs, revParse);
85761
+ return "";
85762
+ }
85763
+ const [remote, branch, status, gitLog] = await Promise.all([
85764
+ [
85628
85765
  "remote",
85629
85766
  "get-url",
85630
85767
  "origin"
85631
- ]),
85632
- runGit(kaos, cwd, ["branch", "--show-current"]),
85633
- runGit(kaos, cwd, ["status", "--porcelain"]),
85634
- runGit(kaos, cwd, [
85768
+ ],
85769
+ [
85770
+ "symbolic-ref",
85771
+ "--short",
85772
+ "HEAD"
85773
+ ],
85774
+ ["status", "--porcelain"],
85775
+ [
85635
85776
  "log",
85636
85777
  "-3",
85637
85778
  "--format=%h %s"
85638
- ])
85639
- ]);
85779
+ ]
85780
+ ].map(async (args) => ({
85781
+ args,
85782
+ result: await runGit(kaos, cwd, args)
85783
+ })));
85784
+ for (const { args, result } of [
85785
+ remote,
85786
+ branch,
85787
+ status,
85788
+ gitLog
85789
+ ]) if (!result.ok) logGitFailure(cwd, args, result);
85790
+ const remoteUrl = stdoutOf(remote.result);
85791
+ const branchName = stdoutOf(branch.result);
85792
+ const dirtyRaw = stdoutOf(status.result);
85793
+ const logRaw = stdoutOf(gitLog.result);
85640
85794
  const sections = [`Working directory: ${cwd}`];
85641
85795
  if (remoteUrl) {
85642
85796
  const safeUrl = sanitizeRemoteUrl(remoteUrl);
@@ -85646,15 +85800,13 @@ async function collectGitContext(kaos, cwd) {
85646
85800
  if (project) sections.push(`Project: ${project}`);
85647
85801
  }
85648
85802
  }
85649
- if (branch) sections.push(`Branch: ${branch}`);
85650
- if (dirtyRaw !== null) {
85651
- const dirtyLines = dirtyRaw.split("\n").filter((line) => line.trim().length > 0);
85652
- if (dirtyLines.length > 0) {
85653
- const total = dirtyLines.length;
85654
- let body = dirtyLines.slice(0, MAX_DIRTY_FILES).map((line) => ` ${line}`).join("\n");
85655
- if (total > MAX_DIRTY_FILES) body += `\n ... and ${String(total - MAX_DIRTY_FILES)} more`;
85656
- sections.push(`Dirty files (${String(total)}):\n${body}`);
85657
- }
85803
+ if (branchName) sections.push(`Branch: ${branchName}`);
85804
+ const dirtyLines = dirtyRaw.split("\n").filter((line) => line.trim().length > 0);
85805
+ if (dirtyLines.length > 0) {
85806
+ const total = dirtyLines.length;
85807
+ let body = dirtyLines.slice(0, MAX_DIRTY_FILES).map((line) => ` ${line}`).join("\n");
85808
+ if (total > MAX_DIRTY_FILES) body += `\n ... and ${String(total - MAX_DIRTY_FILES)} more`;
85809
+ sections.push(`Dirty files (${String(total)}):\n${body}`);
85658
85810
  }
85659
85811
  if (logRaw) {
85660
85812
  const logLines = logRaw.split("\n").filter((line) => line.trim().length > 0);
@@ -85702,39 +85854,87 @@ function tryUrlPath(remoteUrl) {
85702
85854
  return null;
85703
85855
  }
85704
85856
  }
85857
+ function stdoutOf(result) {
85858
+ return result.ok ? result.stdout : "";
85859
+ }
85860
+ function isNotARepo(stderr) {
85861
+ return stderr !== void 0 && stderr.includes("not a git repository");
85862
+ }
85863
+ function logGitFailure(cwd, args, failure) {
85864
+ const command = `git ${args.join(" ")}`;
85865
+ if (failure.kind === "timeout") log.debug("git context command timed out", {
85866
+ cwd,
85867
+ command
85868
+ });
85869
+ else if (failure.kind === "spawn-error") log.warn("git context command failed to spawn", {
85870
+ cwd,
85871
+ command
85872
+ });
85873
+ else log.debug("git context command failed", {
85874
+ cwd,
85875
+ command,
85876
+ exitCode: failure.exitCode,
85877
+ stderr: failure.stderr
85878
+ });
85879
+ }
85705
85880
  /**
85706
- * Run a single `git -C <cwd> <args>` command and return its trimmed stdout,
85707
- * or `null` on any failure (spawn error, non-zero exit, or timeout). The
85708
- * `git -C` form runs in the target directory regardless of the Kaos backend.
85881
+ * Run a single `git -C <cwd> <args>` command and return a structured result.
85882
+ * The `git -C` form runs in the target directory regardless of the Kaos
85883
+ * backend. Both stdout and stderr are captured so callers can tell "not a
85884
+ * git repository" (exit 128 + telltale stderr) apart from other failures.
85709
85885
  */
85710
85886
  async function runGit(kaos, cwd, args) {
85711
85887
  let proc;
85712
85888
  try {
85713
85889
  proc = await kaos.exec("git", "-C", cwd, ...args);
85714
85890
  } catch {
85715
- return null;
85891
+ return {
85892
+ ok: false,
85893
+ kind: "spawn-error"
85894
+ };
85716
85895
  }
85717
85896
  try {
85718
85897
  proc.stdin.end();
85719
85898
  } catch {}
85720
- const work = Promise.all([collectStream(proc.stdout), proc.wait()]);
85899
+ const work = Promise.all([
85900
+ collectStream(proc.stdout),
85901
+ collectStream(proc.stderr),
85902
+ proc.wait()
85903
+ ]);
85721
85904
  work.catch(() => {});
85722
85905
  let timer;
85906
+ let timedOut = false;
85723
85907
  try {
85724
85908
  const timeout = new Promise((_resolve, reject) => {
85725
85909
  timer = setTimeout(() => {
85910
+ timedOut = true;
85726
85911
  reject(/* @__PURE__ */ new Error(`git ${args.join(" ")} timed out`));
85727
85912
  }, GIT_TIMEOUT_MS);
85728
85913
  });
85729
- const [stdout, exitCode] = await Promise.race([work, timeout]);
85730
- if (exitCode !== 0) return null;
85731
- return stdout.trim();
85914
+ const [stdout, stderr, exitCode] = await Promise.race([work, timeout]);
85915
+ if (exitCode !== 0) return {
85916
+ ok: false,
85917
+ kind: "command-failed",
85918
+ exitCode,
85919
+ stderr: stderr.trim()
85920
+ };
85921
+ return {
85922
+ ok: true,
85923
+ stdout: stdout.trim()
85924
+ };
85732
85925
  } catch {
85733
85926
  try {
85734
85927
  await proc.kill("SIGKILL");
85735
85928
  } catch {}
85736
85929
  await work.catch(() => {});
85737
- return null;
85930
+ if (timedOut) return {
85931
+ ok: false,
85932
+ kind: "timeout"
85933
+ };
85934
+ return {
85935
+ ok: false,
85936
+ kind: "command-failed"
85937
+ };
85738
85938
  } finally {
85739
85939
  if (timer !== void 0) clearTimeout(timer);
85740
85940
  if (proc !== void 0) await disposeProcess$2(proc);
@@ -91938,6 +92138,9 @@ var ToolResultBuilder = class {
91938
92138
  get nChars() {
91939
92139
  return this.nCharsValue;
91940
92140
  }
92141
+ get truncated() {
92142
+ return this.truncationHappened;
92143
+ }
91941
92144
  write(text) {
91942
92145
  if (this.nCharsValue >= this.maxChars) {
91943
92146
  if (text.length > 0 && !this.truncationHappened) {
@@ -93556,7 +93759,7 @@ var ReadMediaFileTool = class {
93556
93759
  };
93557
93760
  //#endregion
93558
93761
  //#region ../agent-core/src/tools/builtin/file/write.md?raw
93559
- var write_default = "Create, append to, or replace a file entirely.\n\n- The parent directory must already exist.\n- Mode defaults to overwrite; append adds content at EOF without adding a newline.\n- Write is NOT ALLOWED for incremental changes to existing files, including trivial, one-line, quick, or cosmetic edits. Use Edit instead.\n- Use Write only when the file does not exist, you intend a complete replacement, or the new contents have little continuity with the old contents.\n- Read before overwriting an existing file.\n- Write ignores the Read/Edit line-number view. NEVER include line prefixes.\n- Write outputs content literally, including supplied line endings: \\n stays LF, \\r\\n stays CRLF.\n- For new content too large for one call, overwrite the first chunk, then append subsequent chunks. Never chunk Write to modify an existing file.\n";
93762
+ var write_default = "Create, append to, or replace a file entirely.\n\n- Missing parent directories are created automatically (like `mkdir(parents=True, exist_ok=True)`).\n- Mode defaults to overwrite; append adds content at EOF without adding a newline.\n- Write is NOT ALLOWED for incremental changes to existing files, including trivial, one-line, quick, or cosmetic edits. Use Edit instead.\n- Use Write only when the file does not exist, you intend a complete replacement, or the new contents have little continuity with the old contents.\n- Read before overwriting an existing file.\n- Write ignores the Read/Edit line-number view. NEVER include line prefixes.\n- Write outputs content literally, including supplied line endings: \\n stays LF, \\r\\n stays CRLF.\n- For new content too large for one call, overwrite the first chunk, then append subsequent chunks. Never chunk Write to modify an existing file.\n";
93560
93763
  //#endregion
93561
93764
  //#region ../agent-core/src/tools/builtin/file/write.ts
93562
93765
  /** Mask isolating the file-type bits of a stat mode. */
@@ -93564,7 +93767,7 @@ const S_IFMT = 61440;
93564
93767
  /** File-type bits of a directory. */
93565
93768
  const S_IFDIR = 16384;
93566
93769
  const WriteInputSchema = z.object({
93567
- path: z.string().describe("Path to the file to create, append to, or completely overwrite. Relative paths resolve against the working directory; a path outside the working directory must be absolute. The parent directory must already exist."),
93770
+ path: z.string().describe("Path to the file to create, append to, or completely overwrite. Relative paths resolve against the working directory; a path outside the working directory must be absolute. Missing parent directories are created automatically."),
93568
93771
  content: z.string().describe("Raw full file content to write exactly as provided. This does not use the Read/Edit text view."),
93569
93772
  mode: z.enum(["overwrite", "append"]).optional().describe("Write mode. Defaults to overwrite. append adds content to the end exactly as provided and does not add a newline.")
93570
93773
  });
@@ -93606,7 +93809,7 @@ var WriteTool = class {
93606
93809
  };
93607
93810
  }
93608
93811
  async execution(args, safePath) {
93609
- const parentError = await this.checkParentDirectory(safePath);
93812
+ const parentError = await this.ensureParentDirectory(safePath);
93610
93813
  if (parentError !== void 0) return {
93611
93814
  isError: true,
93612
93815
  output: parentError
@@ -93629,22 +93832,36 @@ var WriteTool = class {
93629
93832
  }
93630
93833
  }
93631
93834
  /**
93632
- * Best-effort check that the parent directory exists and is a directory.
93835
+ * Best-effort check that the parent directory is usable, creating it when
93836
+ * it is missing.
93837
+ *
93838
+ * If the parent (or any ancestor) does not exist, it is created
93839
+ * recursively — mirroring Python's `Path.mkdir(parents=True,
93840
+ * exist_ok=True)` — so the agent does not need a separate `mkdir` round
93841
+ * trip before writing into a fresh subfolder. An existing parent that is
93842
+ * not a directory is still a hard error. Any other `stat` failure
93843
+ * (permissions, an environment without `stat`) is treated as
93844
+ * inconclusive: the check is skipped and the write proceeds, surfacing
93845
+ * the real I/O error if any.
93633
93846
  *
93634
- * The path schema documents this precondition; probing it up front turns a
93635
- * bare `ENOENT` from the underlying write into an actionable message.
93636
93847
  * Returns an error string when the precondition is definitively violated,
93637
- * or `undefined` otherwise. Any other `stat` failure (permissions, an
93638
- * environment without `stat`) is treated as inconclusive: the check is
93639
- * skipped and the write proceeds, surfacing the real I/O error if any.
93848
+ * or `undefined` otherwise.
93640
93849
  */
93641
- async checkParentDirectory(safePath) {
93850
+ async ensureParentDirectory(safePath) {
93642
93851
  const parent = dirname$3(safePath);
93643
93852
  let stat;
93644
93853
  try {
93645
93854
  stat = await this.kaos.stat(parent);
93646
93855
  } catch (error) {
93647
- if (error.code === "ENOENT") return `Parent directory does not exist: ${parent}. Create it before writing this file.`;
93856
+ if (error.code === "ENOENT") try {
93857
+ await this.kaos.mkdir(parent, {
93858
+ parents: true,
93859
+ existOk: true
93860
+ });
93861
+ return;
93862
+ } catch (mkdirError) {
93863
+ return mkdirError instanceof Error ? mkdirError.message : String(mkdirError);
93864
+ }
93648
93865
  return;
93649
93866
  }
93650
93867
  if ((stat.stMode & S_IFMT) !== S_IFDIR) return `Parent path is not a directory: ${parent}.`;
@@ -94030,6 +94247,69 @@ var ToolCallDeduplicator = class {
94030
94247
  }
94031
94248
  };
94032
94249
  //#endregion
94250
+ //#region ../agent-core/src/agent/turn/tool-result-budget.ts
94251
+ const TOOL_RESULT_MAX_CHARS = 5e4;
94252
+ const TOOL_RESULT_PREVIEW_CHARS = 2e3;
94253
+ async function budgetToolResultForModel(options) {
94254
+ const text = persistableToolResultText(options.result.output);
94255
+ if (text === void 0 || text.length <= TOOL_RESULT_MAX_CHARS) return options.result;
94256
+ if (options.result.truncated === true) return options.result;
94257
+ if (options.homedir === void 0) return options.result;
94258
+ const outputPath = await saveToolResult({
94259
+ homedir: options.homedir,
94260
+ toolName: options.toolName,
94261
+ toolCallId: options.toolCallId
94262
+ }, text);
94263
+ if (outputPath === void 0) return options.result;
94264
+ const output = renderPersistedToolResult(options.toolName, options.toolCallId, text, outputPath);
94265
+ return options.result.isError === true ? {
94266
+ ...options.result,
94267
+ output,
94268
+ isError: true
94269
+ } : {
94270
+ ...options.result,
94271
+ output
94272
+ };
94273
+ }
94274
+ function persistableToolResultText(output) {
94275
+ if (typeof output === "string") return output;
94276
+ if (!output.every((part) => part.type === "text")) return;
94277
+ return output.map((part) => part.text).join("");
94278
+ }
94279
+ async function saveToolResult(options, text) {
94280
+ try {
94281
+ const dir = join$1(options.homedir, "tool-results");
94282
+ await mkdir(dir, {
94283
+ recursive: true,
94284
+ mode: 448
94285
+ });
94286
+ const outputPath = join$1(dir, `${safeToolResultFileStem(options.toolName, options.toolCallId)}-${randomUUID()}.txt`);
94287
+ await writeFile$1(outputPath, text, {
94288
+ encoding: "utf8",
94289
+ flag: "wx"
94290
+ });
94291
+ return outputPath;
94292
+ } catch {
94293
+ return;
94294
+ }
94295
+ }
94296
+ function renderPersistedToolResult(toolName, toolCallId, text, outputPath) {
94297
+ const lines = [
94298
+ `Tool output exceeded ${String(TOOL_RESULT_MAX_CHARS)} characters; showing a preview only.`,
94299
+ `tool_name: ${toolName}`,
94300
+ `tool_call_id: ${toolCallId}`,
94301
+ `output_size_chars: ${String(text.length)}`,
94302
+ `output_size_bytes: ${String(Buffer.byteLength(text, "utf8"))}`,
94303
+ `output_path: ${outputPath}`,
94304
+ "next_step: Use Read with output_path to page through the full output."
94305
+ ];
94306
+ lines.push("", "[preview]", text.slice(0, TOOL_RESULT_PREVIEW_CHARS));
94307
+ return lines.join("\n");
94308
+ }
94309
+ function safeToolResultFileStem(toolName, toolCallId) {
94310
+ return `${toolName}-${toolCallId}`.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80) || "tool-result";
94311
+ }
94312
+ //#endregion
94033
94313
  //#region ../agent-core/src/agent/turn/index.ts
94034
94314
  const LLM_NOT_SET_MESSAGE = "LLM not set, send \"/login\" to login";
94035
94315
  /** Origin tag for the synthetic "continue" prompt that drives each goal turn. */
@@ -94568,7 +94848,12 @@ var TurnFlow = class {
94568
94848
  toolOutput: isError === true ? void 0 : toolOutputText(output).slice(0, 2e3)
94569
94849
  }
94570
94850
  });
94571
- return finalResult;
94851
+ return budgetToolResultForModel({
94852
+ homedir: this.agent.homedir,
94853
+ toolName: ctx.toolCall.name,
94854
+ toolCallId: ctx.toolCall.id,
94855
+ result: finalResult
94856
+ });
94572
94857
  }
94573
94858
  }
94574
94859
  })).stopReason;
@@ -95292,7 +95577,7 @@ var BashTool = class {
95292
95577
  },
95293
95578
  approvalRule: literalRulePattern(this.name, args.command),
95294
95579
  matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, args.command),
95295
- execute: ({ signal, onUpdate }) => this.execution(args, signal, onUpdate)
95580
+ execute: ({ signal, onUpdate, onForegroundTaskStart }) => this.execution(args, signal, onUpdate, onForegroundTaskStart)
95296
95581
  };
95297
95582
  }
95298
95583
  spawn(effectiveCwd, command) {
@@ -95314,7 +95599,7 @@ var BashTool = class {
95314
95599
  };
95315
95600
  return this.kaos.execWithEnv(shellArgs, mergedEnv);
95316
95601
  }
95317
- async execution(args, signal, onUpdate) {
95602
+ async execution(args, signal, onUpdate, onForegroundTaskStart) {
95318
95603
  const validationError = this.validateRunRequest(args, signal);
95319
95604
  if (validationError !== void 0) return validationError;
95320
95605
  const startsInBackground = args.run_in_background === true;
@@ -95335,6 +95620,8 @@ var BashTool = class {
95335
95620
  }
95336
95621
  closeProcessStdin(proc);
95337
95622
  let collectForegroundOutput = !startsInBackground;
95623
+ let foregroundOutputPersisted = false;
95624
+ let foregroundTaskId;
95338
95625
  const onProcessOutput = startsInBackground ? void 0 : (kind, text) => {
95339
95626
  if (!collectForegroundOutput) return;
95340
95627
  onUpdate?.({
@@ -95342,14 +95629,20 @@ var BashTool = class {
95342
95629
  text
95343
95630
  });
95344
95631
  builder.write(text);
95632
+ if (!foregroundOutputPersisted && builder.truncated && foregroundTaskId !== void 0) {
95633
+ this.backgroundManager.persistOutput(foregroundTaskId);
95634
+ foregroundOutputPersisted = true;
95635
+ }
95345
95636
  };
95346
95637
  let taskId;
95347
95638
  try {
95348
95639
  taskId = this.backgroundManager.registerTask(new ProcessBackgroundTask(proc, command, description, onProcessOutput), {
95349
95640
  detached: startsInBackground,
95350
95641
  timeoutMs,
95642
+ detachTimeoutMs: DEFAULT_BACKGROUND_TIMEOUT_S * MS_PER_SECOND,
95351
95643
  signal: startsInBackground ? void 0 : signal
95352
95644
  });
95645
+ foregroundTaskId = startsInBackground ? void 0 : taskId;
95353
95646
  } catch (error) {
95354
95647
  collectForegroundOutput = false;
95355
95648
  await killSpawnedProcess(proc);
@@ -95358,6 +95651,7 @@ var BashTool = class {
95358
95651
  output: error instanceof Error ? error.message : String(error)
95359
95652
  };
95360
95653
  }
95654
+ if (!startsInBackground) onForegroundTaskStart?.(taskId);
95361
95655
  if (startsInBackground) return this.backgroundStartedResult(taskId, proc, description, {
95362
95656
  title: "Background task started",
95363
95657
  brief: `Started ${taskId}`
@@ -95370,7 +95664,7 @@ var BashTool = class {
95370
95664
  brief: `Backgrounded ${taskId}`
95371
95665
  }, builder, "foreground_detached");
95372
95666
  }
95373
- return this.foregroundCompletionResult(taskId, proc, builder, foregroundTimeoutMs);
95667
+ return await this.foregroundCompletionResult(taskId, proc, builder, foregroundTimeoutMs);
95374
95668
  } finally {
95375
95669
  collectForegroundOutput = false;
95376
95670
  }
@@ -95394,19 +95688,32 @@ var BashTool = class {
95394
95688
  output: "description is required when run_in_background is true."
95395
95689
  };
95396
95690
  }
95397
- foregroundCompletionResult(taskId, proc, builder, foregroundTimeoutMs) {
95691
+ async foregroundCompletionResult(taskId, proc, builder, foregroundTimeoutMs) {
95398
95692
  const current = this.backgroundManager.getTask(taskId);
95399
95693
  const exitCode = current?.kind === "process" ? current.exitCode : proc.exitCode;
95694
+ let result;
95400
95695
  if (current?.status === "timed_out") {
95401
95696
  const timeoutLabel = formatTimeoutLabel(foregroundTimeoutMs);
95402
- return builder.error(`Command killed by timeout (${timeoutLabel})`, { brief: `Killed by timeout (${timeoutLabel})` });
95697
+ result = builder.error(`Command killed by timeout (${timeoutLabel})`, { brief: `Killed by timeout (${timeoutLabel})` });
95698
+ } else if (current?.status === "killed" && current.stopReason === USER_INTERRUPT_REASON) result = builder.error(USER_INTERRUPT_REASON, { brief: USER_INTERRUPT_REASON });
95699
+ else if ((current?.status === "failed" || current?.status === "killed") && current.stopReason !== void 0) result = builder.error(current.stopReason, { brief: current.stopReason });
95700
+ else if (exitCode === 0) result = builder.ok("Command executed successfully.");
95701
+ else {
95702
+ if (builder.nChars === 0) builder.write(`Process exited with code ${String(exitCode)}`);
95703
+ result = builder.error(`Command failed with exit code: ${String(exitCode)}.`, { brief: `Failed with exit code: ${String(exitCode)}` });
95403
95704
  }
95404
- if (current?.status === "killed" && current.stopReason === USER_INTERRUPT_REASON) return builder.error(USER_INTERRUPT_REASON, { brief: USER_INTERRUPT_REASON });
95405
- if ((current?.status === "failed" || current?.status === "killed") && current.stopReason !== void 0) return builder.error(current.stopReason, { brief: current.stopReason });
95406
- const isError = exitCode !== 0;
95407
- if (isError && builder.nChars === 0) builder.write(`Process exited with code ${String(exitCode)}`);
95408
- if (!isError) return builder.ok("Command executed successfully.");
95409
- return builder.error(`Command failed with exit code: ${String(exitCode)}.`, { brief: `Failed with exit code: ${String(exitCode)}` });
95705
+ return this.addForegroundOutputReference(taskId, result);
95706
+ }
95707
+ async addForegroundOutputReference(taskId, result) {
95708
+ if (!result.truncated) return result;
95709
+ const output = await this.backgroundManager.getOutputSnapshot(taskId, 0);
95710
+ if (!output.fullOutputAvailable || output.outputPath === void 0) return result;
95711
+ const taskOutputHint = this.allowBackground ? `, or TaskOutput(task_id="${taskId}", block=false)` : "";
95712
+ const reference = `\n\n[Full output saved]\ntask_id: ${taskId}\noutput_path: ${output.outputPath}\noutput_size_bytes: ${String(output.outputSizeBytes)}\nnext_step: Use Read with output_path to page through the full log${taskOutputHint}.`;
95713
+ return {
95714
+ ...result,
95715
+ output: `${result.output}${reference}`
95716
+ };
95410
95717
  }
95411
95718
  backgroundStartedResult(taskId, proc, description, labels, builder = new ToolResultBuilder(), scenario = "background_started") {
95412
95719
  const status = this.backgroundManager.getTask(taskId)?.status ?? "running";
@@ -95632,6 +95939,8 @@ function classifySearchError(error) {
95632
95939
  }
95633
95940
  //#endregion
95634
95941
  //#region ../agent-core/src/agent/tool/index.ts
95942
+ /** Foreground timeout (seconds) for a user-initiated `!` shell command. */
95943
+ const SHELL_FOREGROUND_TIMEOUT_S = 120;
95635
95944
  var ToolManager = class {
95636
95945
  agent;
95637
95946
  builtinTools = /* @__PURE__ */ new Map();
@@ -95645,6 +95954,9 @@ var ToolManager = class {
95645
95954
  mcpAccessPatterns = [];
95646
95955
  store = {};
95647
95956
  mcpToolStatusUnsubscribe;
95957
+ /** Abort controllers for in-flight `!` shell commands, keyed by commandId so
95958
+ * the TUI can cancel (Esc / Ctrl+C) a running command. */
95959
+ shellCommandControllers = /* @__PURE__ */ new Map();
95648
95960
  constructor(agent) {
95649
95961
  this.agent = agent;
95650
95962
  this.attachMcpTools();
@@ -95676,6 +95988,95 @@ var ToolManager = class {
95676
95988
  });
95677
95989
  this.store[key] = value;
95678
95990
  }
95991
+ /**
95992
+ * Execute a user-initiated `!` shell command. Reuses the builtin Bash tool
95993
+ * (same kaos / cwd / BackgroundManager as the agent), recording the command
95994
+ * and its output as `shell_command`-origin messages. It does NOT start a turn
95995
+ * — the model is not prompted (parity with claude-code's `shouldQuery: false`).
95996
+ */
95997
+ async runShellCommand(command, commandId) {
95998
+ this.agent.context.appendBashInput(command);
95999
+ const bash = this.builtinTools.get("Bash");
96000
+ if (bash === void 0) {
96001
+ const error = "Bash tool is not available.";
96002
+ this.agent.context.appendBashOutput("", error);
96003
+ return {
96004
+ stdout: "",
96005
+ stderr: error,
96006
+ isError: true
96007
+ };
96008
+ }
96009
+ let stdout = "";
96010
+ let stderr = "";
96011
+ let isError;
96012
+ const controller = new AbortController();
96013
+ if (commandId !== void 0) this.shellCommandControllers.set(commandId, controller);
96014
+ try {
96015
+ const execution = await bash.resolveExecution({
96016
+ command,
96017
+ timeout: SHELL_FOREGROUND_TIMEOUT_S
96018
+ });
96019
+ if (!("execute" in execution)) {
96020
+ const output = typeof execution.output === "string" ? execution.output : "Command failed.";
96021
+ this.agent.context.appendBashOutput("", output);
96022
+ return {
96023
+ stdout: "",
96024
+ stderr: output,
96025
+ isError: true
96026
+ };
96027
+ }
96028
+ const result = await execution.execute({
96029
+ turnId: "",
96030
+ toolCallId: "shell-command",
96031
+ signal: controller.signal,
96032
+ onUpdate: (update) => {
96033
+ if (update.kind === "stdout") stdout += update.text ?? "";
96034
+ else if (update.kind === "stderr") stderr += update.text ?? "";
96035
+ else return;
96036
+ if (commandId !== void 0) this.agent.emitEvent({
96037
+ type: "shell.output",
96038
+ commandId,
96039
+ update
96040
+ });
96041
+ },
96042
+ onForegroundTaskStart: (taskId) => {
96043
+ if (commandId !== void 0) this.agent.emitEvent({
96044
+ type: "shell.started",
96045
+ commandId,
96046
+ taskId
96047
+ });
96048
+ }
96049
+ });
96050
+ isError = result.isError === true;
96051
+ if (typeof result.output === "string" && result.output.startsWith("task_id: ")) {
96052
+ this.agent.context.injectAndNotify(result.output, {
96053
+ kind: "injection",
96054
+ variant: "shell_command_backgrounded"
96055
+ });
96056
+ return {
96057
+ stdout: result.output,
96058
+ stderr: "",
96059
+ isError: false,
96060
+ backgrounded: true
96061
+ };
96062
+ }
96063
+ if (isError && stdout.length === 0 && stderr.length === 0 && typeof result.output === "string" && result.output.length > 0) stderr = result.output;
96064
+ } catch (error) {
96065
+ stderr += error instanceof Error ? error.message : String(error);
96066
+ isError = true;
96067
+ } finally {
96068
+ if (commandId !== void 0) this.shellCommandControllers.delete(commandId);
96069
+ }
96070
+ this.agent.context.appendBashOutput(stdout, stderr, isError);
96071
+ return {
96072
+ stdout,
96073
+ stderr,
96074
+ isError
96075
+ };
96076
+ }
96077
+ cancelShellCommand(commandId) {
96078
+ this.shellCommandControllers.get(commandId)?.abort();
96079
+ }
95679
96080
  registerUserTool(input) {
95680
96081
  this.agent.records.logRecord({
95681
96082
  type: "tools.register_user_tool",
@@ -96392,6 +96793,8 @@ var Agent$1 = class {
96392
96793
  prompt: (payload) => {
96393
96794
  this.turn.prompt(payload.input);
96394
96795
  },
96796
+ runShellCommand: (payload) => this.tools.runShellCommand(payload.command, payload.commandId),
96797
+ cancelShellCommand: (payload) => this.tools.cancelShellCommand(payload.commandId),
96395
96798
  steer: (payload) => {
96396
96799
  this.telemetry.track("input_steer", { parts: payload.input.length });
96397
96800
  this.turn.steer(payload.input);
@@ -125836,7 +126239,7 @@ var StdioMcpClient = class {
125836
126239
  command: config.command,
125837
126240
  args: config.args,
125838
126241
  env: mergeStdioEnv(config.env),
125839
- cwd: config.cwd,
126242
+ cwd: resolveStdioCwd(config.cwd, options.defaultCwd),
125840
126243
  stderr: "pipe"
125841
126244
  });
125842
126245
  this.transport.stderr?.on("data", (chunk) => {
@@ -125949,6 +126352,11 @@ var BoundedTail = class {
125949
126352
  return this.buffer;
125950
126353
  }
125951
126354
  };
126355
+ function resolveStdioCwd(configCwd, defaultCwd) {
126356
+ if (configCwd === void 0) return defaultCwd;
126357
+ if (defaultCwd !== void 0 && !isAbsolute$1(configCwd)) return resolve$2(defaultCwd, configCwd);
126358
+ return configCwd;
126359
+ }
125952
126360
  function mergeStdioEnv(configEnv, parentEnv = process.env) {
125953
126361
  const merged = {};
125954
126362
  for (const [key, value] of Object.entries(parentEnv)) if (value !== void 0) merged[key] = value;
@@ -126186,7 +126594,10 @@ var McpConnectionManager = class {
126186
126594
  }
126187
126595
  createClient(config, name) {
126188
126596
  const toolCallTimeoutMs = config.toolTimeoutMs;
126189
- if (config.transport === "stdio") return new StdioMcpClient(config, { toolCallTimeoutMs });
126597
+ if (config.transport === "stdio") return new StdioMcpClient(config, {
126598
+ toolCallTimeoutMs,
126599
+ defaultCwd: this.options.stdioCwd
126600
+ });
126190
126601
  if (config.transport === "sse") return new SseMcpClient(config, {
126191
126602
  toolCallTimeoutMs,
126192
126603
  envLookup: this.options.envLookup,
@@ -126477,6 +126888,7 @@ var Session$1 = class {
126477
126888
  custom: {}
126478
126889
  };
126479
126890
  writeMetadataPromise = Promise.resolve();
126891
+ agentsMdWarning;
126480
126892
  constructor(options) {
126481
126893
  this.options = options;
126482
126894
  this.logHandle = options.id === void 0 ? void 0 : getRootLogger().attachSession({
@@ -126497,7 +126909,8 @@ var Session$1 = class {
126497
126909
  this.skills = new SessionSkillRegistry({ sessionId: options.id });
126498
126910
  this.mcp = new McpConnectionManager({
126499
126911
  oauthService: new McpOAuthService({ kimiHomeDir: options.kimiHomeDir }),
126500
- log: this.log
126912
+ log: this.log,
126913
+ stdioCwd: options.kaos.getcwd()
126501
126914
  });
126502
126915
  this.mcp.onStatusChange((entry) => {
126503
126916
  this.onMcpServerStatusChange(entry);
@@ -126692,6 +127105,36 @@ var Session$1 = class {
126692
127105
  async bootstrapAgentProfile(agent, profile) {
126693
127106
  const context = await prepareSystemPromptContext(this.systemContextKaos(agent.kaos.getcwd()), this.options.kimiHomeDir, { additionalDirs: this.additionalDirs });
126694
127107
  agent.useProfile(profile, context);
127108
+ const { agentsMdWarning } = context;
127109
+ if (agentsMdWarning !== void 0) {
127110
+ this.agentsMdWarning = agentsMdWarning;
127111
+ log.warn("AGENTS.md exceeds recommended size", { message: agentsMdWarning });
127112
+ agent.emitEvent({
127113
+ type: "warning",
127114
+ message: agentsMdWarning,
127115
+ code: "agents-md-oversized"
127116
+ });
127117
+ }
127118
+ }
127119
+ async getSessionWarnings() {
127120
+ const warnings = [];
127121
+ const agentsMdWarning = await this.computeAgentsMdWarning();
127122
+ if (agentsMdWarning !== void 0) warnings.push({
127123
+ code: "agents-md-oversized",
127124
+ message: agentsMdWarning,
127125
+ severity: "warning"
127126
+ });
127127
+ return warnings;
127128
+ }
127129
+ async computeAgentsMdWarning() {
127130
+ if (this.agentsMdWarning !== void 0) return this.agentsMdWarning;
127131
+ try {
127132
+ const context = await prepareSystemPromptContext(this.systemContextKaos(this.toolKaos.getcwd()), this.options.kimiHomeDir, { additionalDirs: this.additionalDirs });
127133
+ this.agentsMdWarning = context.agentsMdWarning;
127134
+ } catch (error) {
127135
+ log.warn("failed to compute AGENTS.md warning", { error });
127136
+ }
127137
+ return this.agentsMdWarning;
126695
127138
  }
126696
127139
  async generateAgentsMd() {
126697
127140
  await this.skillsReady;
@@ -126715,6 +127158,45 @@ var Session$1 = class {
126715
127158
  throw new KimiError(ErrorCodes.SESSION_INIT_FAILED, error instanceof Error ? error.message : "Init failed", { cause: error });
126716
127159
  }
126717
127160
  }
127161
+ /**
127162
+ * Appends a fresh `<plugin_session_start>` system reminder to the main agent
127163
+ * using the currently enabled plugins, then flushes records so the reminder is
127164
+ * persisted and visible on the wire. Used by the explicit `/reload` flow after
127165
+ * the session has been re-resumed with reloaded plugin state.
127166
+ *
127167
+ * When no plugin session start is currently resolvable but an earlier
127168
+ * When no plugin session start is currently resolvable but the context may still
127169
+ * carry stale plugin guidance — either an earlier `<plugin_session_start>`
127170
+ * reminder, or a compaction summary that may have folded one in — appends a
127171
+ * neutralizing reminder instead, so the model does not keep following stale
127172
+ * plugin instructions and the turn-loop injector does not dedup against them.
127173
+ */
127174
+ async appendPluginSessionStartReminder() {
127175
+ await this.skillsReady;
127176
+ const mainAgent = this.requireMainAgent();
127177
+ const reminder = renderPluginSessionStartReminder({
127178
+ sessionStarts: mainAgent.pluginSessionStarts,
127179
+ registry: mainAgent.skills?.registry,
127180
+ log: mainAgent.log
127181
+ });
127182
+ if (reminder !== void 0) mainAgent.context.appendSystemReminder(`${reminder}\n\nThis supersedes any earlier plugin_session_start reminder in this session.`, {
127183
+ kind: "injection",
127184
+ variant: "plugin_session_start"
127185
+ });
127186
+ else if (this.shouldNeutralizePluginSessionStart(mainAgent)) mainAgent.context.appendSystemReminder("There are currently no active plugin session starts. This supersedes any earlier plugin_session_start reminder in this session.", {
127187
+ kind: "injection",
127188
+ variant: "plugin_session_start"
127189
+ });
127190
+ else return;
127191
+ await mainAgent.records.flush();
127192
+ }
127193
+ shouldNeutralizePluginSessionStart(mainAgent) {
127194
+ return mainAgent.context.history.some((message) => {
127195
+ const kind = message.origin?.kind;
127196
+ if (kind === "injection") return message.origin?.variant === "plugin_session_start";
127197
+ return kind === "compaction_summary";
127198
+ });
127199
+ }
126718
127200
  get hasActiveTurn() {
126719
127201
  for (const agent of this.readyAgents()) if (agent.turn.hasActiveTurn) return true;
126720
127202
  return false;
@@ -126940,9 +127422,10 @@ function createRPC() {
126940
127422
  signal?.throwIfAborted();
126941
127423
  let response;
126942
127424
  try {
127425
+ const handlerResult = signal === void 0 ? fn(rpcPayload) : fn(rpcPayload, { signal });
126943
127426
  response = {
126944
127427
  ok: true,
126945
- value: await abortableRpc(Promise.resolve(fn(rpcPayload)), signal)
127428
+ value: await abortableRpc(Promise.resolve(handlerResult), signal)
126946
127429
  };
126947
127430
  } catch (error) {
126948
127431
  signal?.throwIfAborted();
@@ -141407,6 +141890,9 @@ var SessionAPIImpl = class {
141407
141890
  generateAgentsMd(_payload) {
141408
141891
  return this.session.generateAgentsMd();
141409
141892
  }
141893
+ getSessionWarnings(_payload) {
141894
+ return this.session.getSessionWarnings();
141895
+ }
141410
141896
  addAdditionalDir(payload) {
141411
141897
  return this.session.addAdditionalDir(payload.path, payload.persist);
141412
141898
  }
@@ -141417,6 +141903,12 @@ var SessionAPIImpl = class {
141417
141903
  async steer({ agentId, ...payload }) {
141418
141904
  return (await this.getAgent(agentId)).steer(payload);
141419
141905
  }
141906
+ async runShellCommand({ agentId, ...payload }) {
141907
+ return (await this.getAgent(agentId)).runShellCommand(payload);
141908
+ }
141909
+ async cancelShellCommand({ agentId, ...payload }) {
141910
+ return (await this.getAgent(agentId)).cancelShellCommand(payload);
141911
+ }
141420
141912
  async cancel({ agentId, ...payload }) {
141421
141913
  return (await this.getAgent(agentId)).cancel(payload);
141422
141914
  }
@@ -143273,6 +143765,7 @@ var KimiCore = class {
143273
143765
  throw error;
143274
143766
  }
143275
143767
  this.sessions.set(summary.id, session);
143768
+ if (overrides.forcePluginSessionStartReminder === true) await session.appendPluginSessionStartReminder();
143276
143769
  return resumeSessionResult(summary, session, warning);
143277
143770
  }
143278
143771
  async reloadSession(input) {
@@ -143286,7 +143779,7 @@ var KimiCore = class {
143286
143779
  await active.closeForReload();
143287
143780
  this.sessions.delete(summary.id);
143288
143781
  }
143289
- return this.resumeSession({ sessionId: summary.id });
143782
+ return this.resumeSessionWithOverrides({ sessionId: summary.id }, { forcePluginSessionStartReminder: input.forcePluginSessionStartReminder });
143290
143783
  }
143291
143784
  async forkSession(input) {
143292
143785
  const source = await this.sessionStore.get(input.sessionId);
@@ -143361,6 +143854,12 @@ var KimiCore = class {
143361
143854
  prompt({ sessionId, ...payload }) {
143362
143855
  return this.sessionApi(sessionId).prompt(payload);
143363
143856
  }
143857
+ runShellCommand({ sessionId, ...payload }) {
143858
+ return this.sessionApi(sessionId).runShellCommand(payload);
143859
+ }
143860
+ cancelShellCommand({ sessionId, ...payload }) {
143861
+ return this.sessionApi(sessionId).cancelShellCommand(payload);
143862
+ }
143364
143863
  steer({ sessionId, ...payload }) {
143365
143864
  return this.sessionApi(sessionId).steer(payload);
143366
143865
  }
@@ -143473,6 +143972,9 @@ var KimiCore = class {
143473
143972
  generateAgentsMd({ sessionId, ...payload }) {
143474
143973
  return this.sessionApi(sessionId).generateAgentsMd(payload);
143475
143974
  }
143975
+ getSessionWarnings({ sessionId, ...payload }) {
143976
+ return this.sessionApi(sessionId).getSessionWarnings(payload);
143977
+ }
143476
143978
  addAdditionalDir({ sessionId, ...payload }) {
143477
143979
  return this.requireSession(sessionId).addAdditionalDir(payload.path, payload.persist);
143478
143980
  }
@@ -144279,8 +144781,8 @@ var BridgeClientAPI = class {
144279
144781
  async requestApproval(request) {
144280
144782
  return this.deps.approvalService.request(request);
144281
144783
  }
144282
- async requestQuestion(request) {
144283
- return this.deps.questionService.request(request);
144784
+ async requestQuestion(request, options) {
144785
+ return this.deps.questionService.request(request, options);
144284
144786
  }
144285
144787
  async toolCall(request) {
144286
144788
  return {
@@ -151298,6 +151800,7 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151298
151800
  logger;
151299
151801
  eventService;
151300
151802
  _serviceBrand;
151803
+ homeDir;
151301
151804
  sessionsDir;
151302
151805
  registryPath;
151303
151806
  opQueue = Promise.resolve();
@@ -151305,12 +151808,33 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151305
151808
  super();
151306
151809
  this.logger = logger;
151307
151810
  this.eventService = eventService;
151811
+ this.homeDir = env.homeDir;
151308
151812
  this.sessionsDir = join(env.homeDir, "sessions");
151309
151813
  this.registryPath = join(env.homeDir, WORKSPACE_REGISTRY_FILE);
151310
151814
  }
151311
151815
  async list() {
151312
151816
  const file = await this.runExclusive(() => this.readRegistry());
151313
- return (await Promise.all(Object.entries(file.workspaces).map(([workspaceId, entry]) => this.hydrate(workspaceId, entry)))).sort((a, b) => b.last_opened_at < a.last_opened_at ? -1 : 1);
151817
+ const deleted = new Set(file.deleted_workspace_ids);
151818
+ const result = [];
151819
+ for (const [id, entry] of Object.entries(file.workspaces)) result.push(await this.hydrate(id, entry));
151820
+ const index = await readSessionIndex(this.homeDir, this.sessionsDir);
151821
+ const derived = /* @__PURE__ */ new Map();
151822
+ for (const entry of index.values()) {
151823
+ const id = encodeWorkDirKey(entry.workDir);
151824
+ if (file.workspaces[id] !== void 0 || deleted.has(id)) continue;
151825
+ derived.set(id, entry.workDir);
151826
+ }
151827
+ for (const [id, workDir] of derived) {
151828
+ const sessionCount = await countActiveSessions(join(this.sessionsDir, id));
151829
+ if (sessionCount === 0) continue;
151830
+ result.push(await this.hydrate(id, {
151831
+ root: workDir,
151832
+ name: basename(workDir),
151833
+ created_at: "",
151834
+ last_opened_at: ""
151835
+ }, sessionCount));
151836
+ }
151837
+ return result.sort((a, b) => b.last_opened_at < a.last_opened_at ? -1 : 1);
151314
151838
  }
151315
151839
  async get(workspaceId) {
151316
151840
  const entry = await this.runExclusive(async () => {
@@ -151347,6 +151871,7 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151347
151871
  last_opened_at: now
151348
151872
  };
151349
151873
  file.workspaces[workspaceId] = next;
151874
+ file.deleted_workspace_ids = file.deleted_workspace_ids.filter((id) => id !== workspaceId);
151350
151875
  await this.writeRegistry(file);
151351
151876
  return {
151352
151877
  entry: next,
@@ -151384,10 +151909,18 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151384
151909
  const root = await this.runExclusive(async () => {
151385
151910
  const file = await this.readRegistry();
151386
151911
  const existing = file.workspaces[workspaceId];
151387
- if (existing === void 0) throw new WorkspaceNotFoundError(workspaceId);
151388
- delete file.workspaces[workspaceId];
151912
+ let root;
151913
+ if (existing !== void 0) {
151914
+ delete file.workspaces[workspaceId];
151915
+ root = existing.root;
151916
+ } else {
151917
+ const derived = await this.findDerivedWorkDir(workspaceId);
151918
+ if (derived === void 0) throw new WorkspaceNotFoundError(workspaceId);
151919
+ root = derived;
151920
+ }
151921
+ if (!file.deleted_workspace_ids.includes(workspaceId)) file.deleted_workspace_ids.push(workspaceId);
151389
151922
  await this.writeRegistry(file);
151390
- return existing.root;
151923
+ return root;
151391
151924
  });
151392
151925
  this.publishWorkspace({
151393
151926
  type: "event.workspace.deleted",
@@ -151399,11 +151932,19 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151399
151932
  const entry = await this.runExclusive(async () => {
151400
151933
  return (await this.readRegistry()).workspaces[workspaceId] ?? null;
151401
151934
  });
151402
- if (entry === null) throw new WorkspaceNotFoundError(workspaceId);
151403
- return entry.root;
151935
+ if (entry !== null) return entry.root;
151936
+ const derived = await this.findDerivedWorkDir(workspaceId);
151937
+ if (derived !== void 0) return derived;
151938
+ throw new WorkspaceNotFoundError(workspaceId);
151939
+ }
151940
+ /** Look up a derived workspace's workDir from the session index, or undefined
151941
+ * if the id is not a known derived bucket. */
151942
+ async findDerivedWorkDir(workspaceId) {
151943
+ const index = await readSessionIndex(this.homeDir, this.sessionsDir);
151944
+ for (const e of index.values()) if (encodeWorkDirKey(e.workDir) === workspaceId) return e.workDir;
151404
151945
  }
151405
- async hydrate(workspaceId, entry) {
151406
- const [{ is_git_repo, branch }, session_count] = await Promise.all([detectGit(entry.root), countActiveSessions(join(this.sessionsDir, workspaceId))]);
151946
+ async hydrate(workspaceId, entry, sessionCount) {
151947
+ const [{ is_git_repo, branch }, session_count] = await Promise.all([detectGit(entry.root), sessionCount ?? countActiveSessions(join(this.sessionsDir, workspaceId))]);
151407
151948
  return {
151408
151949
  id: workspaceId,
151409
151950
  root: entry.root,
@@ -151445,7 +151986,8 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151445
151986
  const code = err.code;
151446
151987
  if (code === "ENOENT" || code === "ENOTDIR") return {
151447
151988
  version: WORKSPACE_REGISTRY_VERSION,
151448
- workspaces: {}
151989
+ workspaces: {},
151990
+ deleted_workspace_ids: []
151449
151991
  };
151450
151992
  throw err;
151451
151993
  }
@@ -151459,14 +152001,16 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151459
152001
  }, "workspaces.json malformed; treating as empty");
151460
152002
  return {
151461
152003
  version: WORKSPACE_REGISTRY_VERSION,
151462
- workspaces: {}
152004
+ workspaces: {},
152005
+ deleted_workspace_ids: []
151463
152006
  };
151464
152007
  }
151465
152008
  if (typeof parsed !== "object" || parsed === null || typeof parsed.workspaces !== "object" || parsed.workspaces === null) {
151466
152009
  this.logger.warn({ path: this.registryPath }, "workspaces.json missing required keys; treating as empty");
151467
152010
  return {
151468
152011
  version: WORKSPACE_REGISTRY_VERSION,
151469
- workspaces: {}
152012
+ workspaces: {},
152013
+ deleted_workspace_ids: []
151470
152014
  };
151471
152015
  }
151472
152016
  const rawWorkspaces = parsed.workspaces;
@@ -151475,9 +152019,12 @@ let WorkspaceRegistryService = class WorkspaceRegistryService extends Disposable
151475
152019
  const entry = this.sanitizeEntry(value);
151476
152020
  if (entry !== null) workspaces[id] = entry;
151477
152021
  }
152022
+ const version = typeof parsed.version === "number" ? parsed.version : WORKSPACE_REGISTRY_VERSION;
152023
+ const rawDeleted = parsed.deleted_workspace_ids;
151478
152024
  return {
151479
- version: typeof parsed.version === "number" ? parsed.version : WORKSPACE_REGISTRY_VERSION,
151480
- workspaces
152025
+ version,
152026
+ workspaces,
152027
+ deleted_workspace_ids: Array.isArray(rawDeleted) ? rawDeleted.filter((id) => typeof id === "string") : []
151481
152028
  };
151482
152029
  }
151483
152030
  sanitizeEntry(value) {
@@ -152926,7 +153473,6 @@ let SessionService = class SessionService extends Disposable {
152926
153473
  case "event.question.requested":
152927
153474
  case "event.question.answered":
152928
153475
  case "event.question.dismissed":
152929
- case "event.question.expired":
152930
153476
  this._emitStatusChanged(sessionId);
152931
153477
  break;
152932
153478
  }
@@ -153104,6 +153650,17 @@ let SessionService = class SessionService extends Disposable {
153104
153650
  context_usage: contextUsage
153105
153651
  };
153106
153652
  }
153653
+ async getSessionWarnings(id) {
153654
+ if (!(await this.core.rpc.listSessions({})).some((s) => s.id === id)) throw new SessionNotFoundError(id);
153655
+ try {
153656
+ await this.core.rpc.resumeSession({ sessionId: id });
153657
+ } catch {}
153658
+ try {
153659
+ return await this.core.rpc.getSessionWarnings({ sessionId: id });
153660
+ } catch {
153661
+ return [];
153662
+ }
153663
+ }
153107
153664
  async compact(id, input) {
153108
153665
  if ((await this.core.rpc.listSessions({})).find((s) => s.id === id) === void 0) throw new SessionNotFoundError(id);
153109
153666
  await this.core.rpc.resumeSession({ sessionId: id });
@@ -154345,7 +154902,7 @@ let PromptService = class PromptService extends Disposable {
154345
154902
  });
154346
154903
  }
154347
154904
  async _requireSession(sid) {
154348
- if (!(await this.core.rpc.listSessions({})).some((s) => s.id === sid)) throw new SessionNotFoundError(sid);
154905
+ if ((await this.core.rpc.listSessions({ sessionId: sid })).length === 0) throw new SessionNotFoundError(sid);
154349
154906
  }
154350
154907
  dispose() {
154351
154908
  if (this._store.isDisposed) return;
@@ -155111,9 +155668,12 @@ var Session = class {
155111
155668
  this.ensureOpen();
155112
155669
  return this.resumeState;
155113
155670
  }
155114
- async reloadSession() {
155671
+ async reloadSession(options) {
155115
155672
  this.ensureOpen();
155116
- const summary = await this.rpc.reloadSession({ sessionId: this.id });
155673
+ const summary = await this.rpc.reloadSession({
155674
+ sessionId: this.id,
155675
+ forcePluginSessionStartReminder: options?.forcePluginSessionStartReminder
155676
+ });
155117
155677
  this.summary = summary;
155118
155678
  this.resumeState = resumeStateFromSummary(summary);
155119
155679
  return summary;
@@ -155139,6 +155699,25 @@ var Session = class {
155139
155699
  input: normalizePromptInput(input)
155140
155700
  });
155141
155701
  }
155702
+ /** Execute a user-initiated `!` shell command (silent — does not prompt the
155703
+ * model). Resolves with the command's stdout/stderr for immediate display.
155704
+ * Pass `commandId` to receive live `shell.output` events for this command. */
155705
+ async runShellCommand(command, options) {
155706
+ this.ensureOpen();
155707
+ return this.rpc.runShellCommand({
155708
+ sessionId: this.id,
155709
+ command,
155710
+ commandId: options?.commandId
155711
+ });
155712
+ }
155713
+ /** Cancel a running `!` shell command by its commandId (e.g. on Esc / Ctrl+C). */
155714
+ async cancelShellCommand(commandId) {
155715
+ this.ensureOpen();
155716
+ return this.rpc.cancelShellCommand({
155717
+ sessionId: this.id,
155718
+ commandId
155719
+ });
155720
+ }
155142
155721
  async steer(input) {
155143
155722
  this.ensureOpen();
155144
155723
  await this.rpc.steer({
@@ -155157,6 +155736,10 @@ var Session = class {
155157
155736
  this.ensureOpen();
155158
155737
  await this.rpc.generateAgentsMd({ sessionId: this.id });
155159
155738
  }
155739
+ async getSessionWarnings() {
155740
+ this.ensureOpen();
155741
+ return this.rpc.getSessionWarnings({ sessionId: this.id });
155742
+ }
155160
155743
  async addAdditionalDir(path, options) {
155161
155744
  this.ensureOpen();
155162
155745
  const normalized = normalizeRequiredString(path, "Additional directory cannot be empty", ErrorCodes.REQUEST_INVALID);
@@ -155572,11 +156155,14 @@ var KimiHarness = class {
155572
156155
  const id = normalizeSessionId(input.id);
155573
156156
  const active = this.activeSessions.get(id);
155574
156157
  if (active !== void 0) {
155575
- await active.reloadSession();
156158
+ await active.reloadSession({ forcePluginSessionStartReminder: input.forcePluginSessionStartReminder });
155576
156159
  this.trackSessionEvent(active.id, "session_reload");
155577
156160
  return active;
155578
156161
  }
155579
- const summary = await this.rpc.reloadSession({ sessionId: id });
156162
+ const summary = await this.rpc.reloadSession({
156163
+ sessionId: id,
156164
+ forcePluginSessionStartReminder: input.forcePluginSessionStartReminder
156165
+ });
155580
156166
  const session = new Session({
155581
156167
  id: summary.id,
155582
156168
  workDir: summary.workDir,
@@ -155849,7 +156435,10 @@ var SDKRpcClientBase = class {
155849
156435
  return this.resumeSession(input);
155850
156436
  }
155851
156437
  async reloadSession(input) {
155852
- return (await this.getRpc()).reloadSession({ sessionId: input.sessionId });
156438
+ return (await this.getRpc()).reloadSession({
156439
+ sessionId: input.sessionId,
156440
+ forcePluginSessionStartReminder: input.forcePluginSessionStartReminder
156441
+ });
155853
156442
  }
155854
156443
  async forkSession(input) {
155855
156444
  return (await this.getRpc()).forkSession({
@@ -155904,6 +156493,23 @@ var SDKRpcClientBase = class {
155904
156493
  input: input.input
155905
156494
  });
155906
156495
  }
156496
+ async runShellCommand(input) {
156497
+ const agentId = this.interactiveAgentId;
156498
+ return (await this.getRpc()).runShellCommand({
156499
+ sessionId: input.sessionId,
156500
+ agentId,
156501
+ command: input.command,
156502
+ commandId: input.commandId
156503
+ });
156504
+ }
156505
+ async cancelShellCommand(input) {
156506
+ const agentId = this.interactiveAgentId;
156507
+ return (await this.getRpc()).cancelShellCommand({
156508
+ sessionId: input.sessionId,
156509
+ agentId,
156510
+ commandId: input.commandId
156511
+ });
156512
+ }
155907
156513
  async steer(input) {
155908
156514
  const agentId = this.interactiveAgentId;
155909
156515
  return (await this.getRpc()).steer({
@@ -155915,6 +156521,9 @@ var SDKRpcClientBase = class {
155915
156521
  async generateAgentsMd(input) {
155916
156522
  return (await this.getRpc()).generateAgentsMd({ sessionId: input.sessionId });
155917
156523
  }
156524
+ async getSessionWarnings(input) {
156525
+ return (await this.getRpc()).getSessionWarnings({ sessionId: input.sessionId });
156526
+ }
155918
156527
  async addAdditionalDir(input) {
155919
156528
  return (await this.getRpc()).addAdditionalDir({
155920
156529
  sessionId: input.id,