@khalilgharbaoui/opencode-claude-code-plugin 0.4.18 → 0.4.20

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.js CHANGED
@@ -198,7 +198,12 @@ var OPENCODE_HANDLED_TOOLS = /* @__PURE__ */ new Set([
198
198
  var CLAUDE_INTERNAL_TOOLS = /* @__PURE__ */ new Set([
199
199
  "ToolSearch",
200
200
  "Agent",
201
- "AskFollowupQuestion"
201
+ "AskFollowupQuestion",
202
+ "TaskCreate",
203
+ "TaskUpdate",
204
+ "TaskList",
205
+ "TaskGet",
206
+ "TaskStop"
202
207
  ]);
203
208
  function mapTool(name, input, opts) {
204
209
  if (CLAUDE_INTERNAL_TOOLS.has(name)) {
@@ -340,7 +345,74 @@ function getToolResultText(part) {
340
345
  return JSON.stringify(value);
341
346
  }
342
347
  }
343
- function compactConversationHistory(prompt) {
348
+ var MAX_HISTORY_CHARS = 18e4;
349
+ var MAX_TOOL_RESULT_CHARS = 1e4;
350
+ var MAX_TOOL_INPUT_CHARS = 2e3;
351
+ function clipWithMarker(text, max) {
352
+ if (text.length <= max) return text;
353
+ return `${text.slice(0, max)}
354
+ \u2026[truncated ${text.length - max} chars]`;
355
+ }
356
+ function renderToolInput(input) {
357
+ let raw;
358
+ try {
359
+ raw = typeof input === "string" ? input : JSON.stringify(input);
360
+ } catch {
361
+ raw = String(input);
362
+ }
363
+ return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
364
+ }
365
+ function renderMessageContentForCompaction(msg) {
366
+ const lines = [];
367
+ let toolResultCount = 0;
368
+ if (typeof msg.content === "string") {
369
+ return { text: msg.content, toolResultCount: 0 };
370
+ }
371
+ if (!Array.isArray(msg.content)) {
372
+ return { text: "", toolResultCount: 0 };
373
+ }
374
+ for (const part of msg.content) {
375
+ if (!part) continue;
376
+ switch (part.type) {
377
+ case "text":
378
+ if (part.text) lines.push(part.text);
379
+ break;
380
+ case "tool-call":
381
+ lines.push(
382
+ `[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
383
+ );
384
+ break;
385
+ case "tool-result":
386
+ toolResultCount++;
387
+ lines.push(
388
+ `[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
389
+ ${clipWithMarker(
390
+ getToolResultText(part),
391
+ MAX_TOOL_RESULT_CHARS
392
+ )}`
393
+ );
394
+ break;
395
+ case "image":
396
+ lines.push(
397
+ `[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
398
+ );
399
+ break;
400
+ case "file":
401
+ lines.push(
402
+ `[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
403
+ );
404
+ break;
405
+ case "reasoning":
406
+ break;
407
+ }
408
+ }
409
+ return { text: lines.join("\n"), toolResultCount };
410
+ }
411
+ function compactConversationHistory(prompt, opts = {}) {
412
+ const mode = opts.mode ?? "fresh-session";
413
+ if (mode === "compaction") {
414
+ return buildCompactionHistory(prompt);
415
+ }
344
416
  const conversationMessages = prompt.filter(
345
417
  (m) => m.role === "user" || m.role === "assistant"
346
418
  );
@@ -382,9 +454,59 @@ function compactConversationHistory(prompt) {
382
454
  }
383
455
  return historyParts.join("\n\n");
384
456
  }
385
- function getClaudeUserMessage(prompt, includeHistoryContext = false, reasoningEffort) {
457
+ function buildCompactionHistory(prompt) {
458
+ const entries = [];
459
+ let total = 0;
460
+ let totalToolResults = 0;
461
+ let droppedOldest = 0;
462
+ const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
463
+ for (let i = end - 1; i >= 0; i--) {
464
+ const msg = prompt[i];
465
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
466
+ const { text, toolResultCount } = renderMessageContentForCompaction(msg);
467
+ if (!text.trim()) continue;
468
+ const entry = `${roleLabel}: ${text}`;
469
+ if (total + entry.length > MAX_HISTORY_CHARS) {
470
+ droppedOldest = i + 1;
471
+ break;
472
+ }
473
+ entries.push(entry);
474
+ total += entry.length + 2;
475
+ totalToolResults += toolResultCount;
476
+ }
477
+ if (entries.length === 0) return null;
478
+ entries.reverse();
479
+ log.info("built compaction history", {
480
+ entries: entries.length,
481
+ chars: total,
482
+ toolResults: totalToolResults,
483
+ droppedOldestBefore: droppedOldest
484
+ });
485
+ return entries.join("\n\n");
486
+ }
487
+ function getClaudeUserMessage(prompt, includeHistoryContext = false, reasoningEffort, opts = {}) {
488
+ const compactionMode = opts.compactionMode === true;
386
489
  const content = [];
387
- if (includeHistoryContext) {
490
+ if (compactionMode) {
491
+ const transcript = compactConversationHistory(prompt, {
492
+ mode: "compaction"
493
+ });
494
+ if (transcript) {
495
+ log.info("including compaction transcript", {
496
+ historyLength: transcript.length
497
+ });
498
+ content.push({
499
+ type: "text",
500
+ text: `<conversation_transcript>
501
+ ${transcript}
502
+ </conversation_transcript>
503
+
504
+ The complete prior conversation appears above. The synthesis instructions follow below.
505
+
506
+ `
507
+ });
508
+ }
509
+ } else if (includeHistoryContext) {
388
510
  const historyContext = compactConversationHistory(prompt);
389
511
  if (historyContext) {
390
512
  log.info("including conversation history context", {
@@ -467,17 +589,19 @@ Now continuing with the current message:
467
589
  }
468
590
  });
469
591
  }
470
- const keyword = reasoningKeyword(reasoningEffort);
471
- if (keyword) {
472
- const lastTextPart = [...content].reverse().find((p) => p.type === "text");
473
- if (lastTextPart) {
474
- lastTextPart.text = lastTextPart.text ? `${lastTextPart.text}
592
+ if (!compactionMode) {
593
+ const keyword = reasoningKeyword(reasoningEffort);
594
+ if (keyword) {
595
+ const lastTextPart = [...content].reverse().find((p) => p.type === "text");
596
+ if (lastTextPart) {
597
+ lastTextPart.text = lastTextPart.text ? `${lastTextPart.text}
475
598
 
476
599
  (${keyword})` : `(${keyword})`;
477
- } else {
478
- content.push({ type: "text", text: `(${keyword})` });
600
+ } else {
601
+ content.push({ type: "text", text: `(${keyword})` });
602
+ }
603
+ log.debug("injected reasoning keyword", { effort: reasoningEffort, keyword });
479
604
  }
480
- log.debug("injected reasoning keyword", { effort: reasoningEffort, keyword });
481
605
  }
482
606
  return JSON.stringify({
483
607
  type: "user",
@@ -922,9 +1046,87 @@ import { spawn } from "child_process";
922
1046
  import { createInterface } from "readline";
923
1047
  import { EventEmitter } from "events";
924
1048
  import { unlink } from "fs/promises";
1049
+
1050
+ // src/cli-version.ts
1051
+ import { execFile } from "child_process";
1052
+ import { promisify } from "util";
1053
+ var execFileAsync = promisify(execFile);
1054
+ var cache = /* @__PURE__ */ new Map();
1055
+ function detectCliVersion(cliPath) {
1056
+ const cached = cache.get(cliPath);
1057
+ if (cached) return cached;
1058
+ const promise = (async () => {
1059
+ try {
1060
+ const { stdout } = await execFileAsync(cliPath, ["--version"], {
1061
+ timeout: 5e3
1062
+ });
1063
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
1064
+ if (!match) {
1065
+ log.warn("claude --version output unparseable", { stdout: stdout.trim() });
1066
+ return null;
1067
+ }
1068
+ const v = {
1069
+ major: Number(match[1]),
1070
+ minor: Number(match[2]),
1071
+ patch: Number(match[3]),
1072
+ raw: stdout.trim()
1073
+ };
1074
+ log.info("detected claude cli version", { cliPath, version: v.raw });
1075
+ if (!cliSupportsThinkingDisplay(v)) {
1076
+ log.notice(
1077
+ "claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
1078
+ { version: v.raw }
1079
+ );
1080
+ }
1081
+ return v;
1082
+ } catch (err) {
1083
+ log.warn("failed to detect claude cli version", {
1084
+ cliPath,
1085
+ error: err instanceof Error ? err.message : String(err)
1086
+ });
1087
+ return null;
1088
+ }
1089
+ })();
1090
+ cache.set(cliPath, promise);
1091
+ return promise;
1092
+ }
1093
+ function gte(v, target) {
1094
+ if (v.major !== target.major) return v.major > target.major;
1095
+ if (v.minor !== target.minor) return v.minor > target.minor;
1096
+ return v.patch >= target.patch;
1097
+ }
1098
+ function cliSupportsThinkingDisplay(v) {
1099
+ if (!v) return false;
1100
+ return gte(v, { major: 2, minor: 1, patch: 142 });
1101
+ }
1102
+ function cliSupportsThinking(v) {
1103
+ if (!v) return false;
1104
+ return gte(v, { major: 2, minor: 0, patch: 0 });
1105
+ }
1106
+
1107
+ // src/session-manager.ts
925
1108
  var activeProcesses = /* @__PURE__ */ new Map();
926
1109
  var claudeSessions = /* @__PURE__ */ new Map();
927
1110
  var MAX_ACTIVE_PROCESSES = 16;
1111
+ function envFlagEnabled(value) {
1112
+ if (value === void 0) return false;
1113
+ const normalized = value.trim().toLowerCase();
1114
+ if (!normalized) return false;
1115
+ return !["0", "false", "no", "off"].includes(normalized);
1116
+ }
1117
+ function isClaudeThinkingDisabled() {
1118
+ return envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_THINKING) || envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING);
1119
+ }
1120
+ function claudeSpawnEnv() {
1121
+ const env = {
1122
+ ...process.env,
1123
+ TERM: "xterm-256color"
1124
+ };
1125
+ if (!isClaudeThinkingDisabled() && process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0) {
1126
+ env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES = "1";
1127
+ }
1128
+ return env;
1129
+ }
928
1130
  function touch(key) {
929
1131
  const existing = activeProcesses.get(key);
930
1132
  if (existing) {
@@ -968,7 +1170,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
968
1170
  const proc = spawn(cliPath, cliArgs, {
969
1171
  cwd,
970
1172
  stdio: ["pipe", "pipe", "pipe"],
971
- env: { ...process.env, TERM: "xterm-256color" },
1173
+ env: claudeSpawnEnv(),
972
1174
  shell: process.platform === "win32"
973
1175
  });
974
1176
  const lineEmitter = new EventEmitter();
@@ -1029,7 +1231,10 @@ function buildCliArgs(opts) {
1029
1231
  mcpConfig,
1030
1232
  strictMcpConfig,
1031
1233
  disallowedTools,
1032
- appendSystemPromptFile
1234
+ appendSystemPromptFile,
1235
+ thinking,
1236
+ thinkingDisplay,
1237
+ cliVersion
1033
1238
  } = opts;
1034
1239
  const args = [
1035
1240
  "--print",
@@ -1065,6 +1270,12 @@ function buildCliArgs(opts) {
1065
1270
  if (disallowedTools && disallowedTools.length > 0) {
1066
1271
  args.push("--disallowedTools", ...disallowedTools);
1067
1272
  }
1273
+ if (thinking && cliSupportsThinking(cliVersion ?? null)) {
1274
+ args.push("--thinking", thinking);
1275
+ }
1276
+ if (thinkingDisplay && cliSupportsThinkingDisplay(cliVersion ?? null)) {
1277
+ args.push("--thinking-display", thinkingDisplay);
1278
+ }
1068
1279
  if (appendSystemPromptFile) {
1069
1280
  args.push("--append-system-prompt-file", appendSystemPromptFile);
1070
1281
  }
@@ -1316,8 +1527,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1316
1527
  });
1317
1528
  } catch (error) {
1318
1529
  const errorMessage = error instanceof Error ? error.message : String(error);
1319
- const isTimeout = errorMessage.includes("timed out after") && errorMessage.includes("waiting for opencode to resolve");
1320
- const logFn = isTimeout ? log.notice : log.warn;
1530
+ const isExpectedCleanup = errorMessage.includes("timed out after") && errorMessage.includes("waiting for opencode to resolve") || errorMessage.includes("rejecting as orphaned") || errorMessage.includes("was orphaned by a new user turn");
1531
+ const logFn = isExpectedCleanup ? log.notice : log.warn;
1321
1532
  logFn("proxy-mcp error handling request", {
1322
1533
  error: errorMessage
1323
1534
  });
@@ -1549,7 +1760,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
1549
1760
  indexRemove(pending.sessionKey, toolCallId);
1550
1761
  clearTimeout(pending.timer);
1551
1762
  pending.reject(error);
1552
- log.warn("rejected pending proxy call", {
1763
+ log.notice("rejected pending proxy call", {
1553
1764
  sessionKey: pending.sessionKey,
1554
1765
  toolCallId: pending.toolCallId,
1555
1766
  toolName: pending.toolName,
@@ -1574,6 +1785,20 @@ import { unlink as unlink2 } from "fs/promises";
1574
1785
  import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
1575
1786
  import { randomUUID as randomUUID2 } from "crypto";
1576
1787
  import { dirname as dirname3, join as join5 } from "path";
1788
+ var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
1789
+ function resolveCompactionModel(configured) {
1790
+ const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
1791
+ if (env) return env;
1792
+ const trimmed = configured?.trim();
1793
+ if (trimmed) return trimmed;
1794
+ return DEFAULT_COMPACTION_MODEL;
1795
+ }
1796
+ var KNOWN_DELTA_TYPES = /* @__PURE__ */ new Set([
1797
+ "thinking_delta",
1798
+ "text_delta",
1799
+ "input_json_delta",
1800
+ "signature_delta"
1801
+ ]);
1577
1802
  function hasNewUserContent(prompt) {
1578
1803
  for (let i = prompt.length - 1; i >= 0; i--) {
1579
1804
  const msg = prompt[i];
@@ -2015,6 +2240,32 @@ var ClaudeCodeLanguageModel = class {
2015
2240
  ];
2016
2241
  return valid.includes(effort) ? effort : void 0;
2017
2242
  }
2243
+ getOpencodeAgent(providerOptions) {
2244
+ if (!providerOptions) return void 0;
2245
+ const ownKey = this.config.provider;
2246
+ const bag = providerOptions[ownKey] ?? providerOptions["claude-code"];
2247
+ const agent = bag?.opencodeAgent;
2248
+ return typeof agent === "string" ? agent : void 0;
2249
+ }
2250
+ isCompactionCall(options) {
2251
+ return this.getOpencodeAgent(options.providerOptions) === "compaction";
2252
+ }
2253
+ /**
2254
+ * Pick the model used to handle /compact. Precedence:
2255
+ * 1. `CLAUDE_CODE_COMPACTION_MODEL` env var (per-process override)
2256
+ * 2. `compactionModel` provider setting (opencode.json / .jsonc)
2257
+ * 3. Built-in default (claude-haiku-4-5)
2258
+ */
2259
+ resolveCompactionModel() {
2260
+ return resolveCompactionModel(this.config.compactionModel);
2261
+ }
2262
+ thinkingCliOptions() {
2263
+ if (isClaudeThinkingDisabled()) return {};
2264
+ return {
2265
+ thinking: "enabled",
2266
+ thinkingDisplay: process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0 ? "summarized" : void 0
2267
+ };
2268
+ }
2018
2269
  latestUserText(prompt) {
2019
2270
  for (let i = prompt.length - 1; i >= 0; i--) {
2020
2271
  const msg = prompt[i];
@@ -2138,10 +2389,19 @@ var ClaudeCodeLanguageModel = class {
2138
2389
  const scope = this.requestScope(options);
2139
2390
  const affinity = this.sessionAffinity(options);
2140
2391
  const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
2392
+ const compactionMode = this.isCompactionCall(options);
2141
2393
  if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
2142
2394
  return this.doGenerateViaStream(options);
2143
2395
  }
2396
+ if (compactionMode) {
2397
+ return this.doGenerateViaStream(options);
2398
+ }
2144
2399
  if (scope === "no-tools") {
2400
+ log.info("doGenerate no-tools title stub", {
2401
+ compactionMode,
2402
+ opencodeAgent: this.getOpencodeAgent(options.providerOptions),
2403
+ providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
2404
+ });
2145
2405
  const text = this.synthesizeTitle(options.prompt);
2146
2406
  return {
2147
2407
  content: [{ type: "text", text }],
@@ -2193,7 +2453,10 @@ var ClaudeCodeLanguageModel = class {
2193
2453
  includeHistoryContext,
2194
2454
  reasoningEffort
2195
2455
  );
2196
- const runtimeStatus = await getRuntimeMcpStatus();
2456
+ const [runtimeStatus, cliVersion] = await Promise.all([
2457
+ getRuntimeMcpStatus(),
2458
+ detectCliVersion(this.config.cliPath)
2459
+ ]);
2197
2460
  const systemPromptFile = buildAppendedSystemPrompt(
2198
2461
  cwd,
2199
2462
  this.config.multiStepContinuation !== false
@@ -2207,7 +2470,9 @@ var ClaudeCodeLanguageModel = class {
2207
2470
  mcpConfig: this.effectiveMcpConfig(cwd, void 0, runtimeStatus).paths,
2208
2471
  strictMcpConfig: this.config.strictMcpConfig,
2209
2472
  disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
2210
- appendSystemPromptFile: systemPromptFile
2473
+ appendSystemPromptFile: systemPromptFile,
2474
+ ...this.thinkingCliOptions(),
2475
+ cliVersion
2211
2476
  });
2212
2477
  log.info("doGenerate starting", {
2213
2478
  cwd,
@@ -2220,7 +2485,7 @@ var ClaudeCodeLanguageModel = class {
2220
2485
  const proc = spawn2(this.config.cliPath, cliArgs, {
2221
2486
  cwd,
2222
2487
  stdio: ["pipe", "pipe", "pipe"],
2223
- env: { ...process.env, TERM: "xterm-256color" },
2488
+ env: claudeSpawnEnv(),
2224
2489
  shell: process.platform === "win32"
2225
2490
  });
2226
2491
  if (systemPromptFile) {
@@ -2435,11 +2700,18 @@ ${plan}
2435
2700
  const skipPermissions = this.config.skipPermissions !== false;
2436
2701
  const scope = this.requestScope(options);
2437
2702
  const affinity = this.sessionAffinity(options);
2438
- const sk = sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
2703
+ const compactionMode = this.isCompactionCall(options);
2704
+ const effectiveModelId = compactionMode ? this.resolveCompactionModel() : this.modelId;
2705
+ const sk = compactionMode ? sessionKey(cwd, `${effectiveModelId}::compaction::${affinity}`) : sessionKey(cwd, `${this.modelId}::${scope}::${affinity}`);
2439
2706
  const toUsage = this.toUsage.bind(this);
2440
2707
  const toFinishReason = this.toFinishReason.bind(this);
2441
2708
  const handleControlRequest = this.handleControlRequest.bind(this);
2442
- if (scope === "no-tools") {
2709
+ if (scope === "no-tools" && !compactionMode) {
2710
+ log.info("doStream no-tools title stub", {
2711
+ compactionMode,
2712
+ opencodeAgent: this.getOpencodeAgent(options.providerOptions),
2713
+ providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
2714
+ });
2443
2715
  const text = this.synthesizeTitle(options.prompt);
2444
2716
  const textId = generateId();
2445
2717
  const stream2 = new ReadableStream({
@@ -2501,11 +2773,12 @@ ${plan}
2501
2773
  const userMsg = getClaudeUserMessage(
2502
2774
  options.prompt,
2503
2775
  includeHistoryContext,
2504
- reasoningEffort
2776
+ reasoningEffort,
2777
+ { compactionMode }
2505
2778
  );
2506
- const resolvedProxy = this.resolvedProxyTools();
2779
+ const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
2507
2780
  const self = this;
2508
- const previousPendingProxyCalls = getPendingProxyCalls(sk);
2781
+ const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
2509
2782
  const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
2510
2783
  call,
2511
2784
  result: this.extractPendingProxyResult(options.prompt, call.toolCallId)
@@ -2513,23 +2786,34 @@ ${plan}
2513
2786
  const hasMatchedPendingResults = previousPendingProxyMatches.some(
2514
2787
  (m) => m.result !== null
2515
2788
  );
2516
- const runtimeStatus = await getRuntimeMcpStatus();
2789
+ const [runtimeStatus, cliVersion] = await Promise.all([
2790
+ compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus(),
2791
+ detectCliVersion(this.config.cliPath)
2792
+ ]);
2517
2793
  log.info("doStream starting", {
2518
2794
  cwd,
2519
- model: this.modelId,
2795
+ model: effectiveModelId,
2520
2796
  textLength: userMsg.length,
2521
2797
  includeHistoryContext,
2522
2798
  hasActiveProcess,
2523
2799
  reasoningEffort,
2524
- proxyTools: resolvedProxy?.map((t) => t.name) ?? null
2800
+ proxyTools: resolvedProxy?.map((t) => t.name) ?? null,
2801
+ compactionMode,
2802
+ scope,
2803
+ opencodeAgent: this.getOpencodeAgent(options.providerOptions),
2804
+ providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
2525
2805
  });
2526
2806
  const stream = new ReadableStream({
2527
2807
  start(controller) {
2808
+ if (compactionMode) {
2809
+ deleteActiveProcess(sk);
2810
+ deleteClaudeSessionId(sk);
2811
+ }
2528
2812
  let activeProcess = getActiveProcess(sk);
2529
2813
  let proc;
2530
2814
  let lineEmitter;
2531
2815
  let proxyServer = activeProcess?.proxyServer ?? null;
2532
- if (activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
2816
+ if (!compactionMode && activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
2533
2817
  const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
2534
2818
  const previousHash = activeProcess.mcpHash ?? null;
2535
2819
  if (previousHash !== probe.bridgedHash) {
@@ -2544,44 +2828,64 @@ ${plan}
2544
2828
  }
2545
2829
  }
2546
2830
  const setup = async () => {
2547
- const discovery = self.effectiveMcpConfig(
2548
- cwd,
2549
- void 0,
2550
- runtimeStatus
2551
- );
2552
- const proxyMcpTools = await self.resolvedProxyMcpTools(
2553
- discovery.allEnabledServerNames
2554
- );
2555
- const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
2556
- const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
2557
- if (!proxyServer && combinedProxyTools) {
2558
- proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
2831
+ let cliArgs;
2832
+ let spawnSystemPromptFile;
2833
+ let spawnProxyServer = null;
2834
+ let spawnMcpHash = null;
2835
+ if (compactionMode) {
2836
+ cliArgs = buildCliArgs({
2837
+ sessionKey: sk,
2838
+ skipPermissions,
2839
+ includeSessionId: false,
2840
+ model: effectiveModelId,
2841
+ permissionMode: self.config.permissionMode,
2842
+ cliVersion
2843
+ });
2844
+ } else {
2845
+ const discovery = self.effectiveMcpConfig(
2846
+ cwd,
2847
+ void 0,
2848
+ runtimeStatus
2849
+ );
2850
+ const proxyMcpTools = await self.resolvedProxyMcpTools(
2851
+ discovery.allEnabledServerNames
2852
+ );
2853
+ const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
2854
+ const combinedProxyTools = resolvedProxy || proxyMcpTools ? [...resolvedProxy ?? [], ...proxyMcpTools ?? []] : null;
2855
+ if (!proxyServer && combinedProxyTools) {
2856
+ proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
2857
+ }
2858
+ const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
2859
+ const extraDisallowed = [];
2860
+ if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
2861
+ const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
2862
+ const mcp = self.effectiveMcpConfig(
2863
+ cwd,
2864
+ proxyServer?.configPath(),
2865
+ runtimeStatus,
2866
+ excludeServers
2867
+ );
2868
+ const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
2869
+ cwd,
2870
+ self.config.multiStepContinuation !== false
2871
+ );
2872
+ cliArgs = buildCliArgs({
2873
+ sessionKey: sk,
2874
+ skipPermissions,
2875
+ model: self.modelId,
2876
+ permissionMode: self.config.permissionMode,
2877
+ mcpConfig: mcp.paths,
2878
+ strictMcpConfig: self.config.strictMcpConfig,
2879
+ disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
2880
+ appendSystemPromptFile: systemPromptFile,
2881
+ ...self.thinkingCliOptions(),
2882
+ cliVersion
2883
+ });
2884
+ spawnSystemPromptFile = systemPromptFile;
2885
+ spawnProxyServer = proxyServer;
2886
+ spawnMcpHash = mcp.bridgedHash;
2559
2887
  }
2560
- const proxyDisallowed = resolvedProxy ? disallowedToolFlags(resolvedProxy) : [];
2561
- const extraDisallowed = [];
2562
- if (self.config.webSearch === "disabled") extraDisallowed.push("WebSearch");
2563
- const allDisallowed = [...proxyDisallowed, ...extraDisallowed];
2564
- const mcp = self.effectiveMcpConfig(
2565
- cwd,
2566
- proxyServer?.configPath(),
2567
- runtimeStatus,
2568
- excludeServers
2569
- );
2570
- const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
2571
- cwd,
2572
- self.config.multiStepContinuation !== false
2573
- );
2574
- const cliArgs = buildCliArgs({
2575
- sessionKey: sk,
2576
- skipPermissions,
2577
- model: self.modelId,
2578
- permissionMode: self.config.permissionMode,
2579
- mcpConfig: mcp.paths,
2580
- strictMcpConfig: self.config.strictMcpConfig,
2581
- disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
2582
- appendSystemPromptFile: systemPromptFile
2583
- });
2584
- if (activeProcess) {
2888
+ if (activeProcess && !compactionMode) {
2585
2889
  proc = activeProcess.proc;
2586
2890
  lineEmitter = activeProcess.lineEmitter;
2587
2891
  log.debug("reusing active process", { sk });
@@ -2591,9 +2895,9 @@ ${plan}
2591
2895
  cliArgs,
2592
2896
  cwd,
2593
2897
  sk,
2594
- proxyServer,
2595
- mcp.bridgedHash,
2596
- systemPromptFile
2898
+ spawnProxyServer,
2899
+ spawnMcpHash,
2900
+ spawnSystemPromptFile
2597
2901
  );
2598
2902
  proc = ap.proc;
2599
2903
  lineEmitter = ap.lineEmitter;
@@ -2619,6 +2923,7 @@ ${plan}
2619
2923
  };
2620
2924
  const reasoningIds = /* @__PURE__ */ new Map();
2621
2925
  const reasoningStarted = /* @__PURE__ */ new Map();
2926
+ let hadThinkingTextFromStream = false;
2622
2927
  let turnCompleted = false;
2623
2928
  let controllerClosed = false;
2624
2929
  let pendingProxyUnsubscribe = null;
@@ -2765,11 +3070,6 @@ ${plan}
2765
3070
  noteReasoning();
2766
3071
  const reasoningId = generateId();
2767
3072
  reasoningIds.set(idx, reasoningId);
2768
- controller.enqueue({
2769
- type: "reasoning-start",
2770
- id: reasoningId
2771
- });
2772
- reasoningStarted.set(idx, true);
2773
3073
  }
2774
3074
  if (block.type === "text") {
2775
3075
  textBlockIndices.add(idx);
@@ -2819,8 +3119,16 @@ ${plan}
2819
3119
  const idx = msg.index;
2820
3120
  if (delta.type === "thinking_delta" && delta.thinking) {
2821
3121
  noteReasoning();
3122
+ hadThinkingTextFromStream = true;
2822
3123
  const reasoningId = reasoningIds.get(idx);
2823
3124
  if (reasoningId) {
3125
+ if (!reasoningStarted.get(idx)) {
3126
+ controller.enqueue({
3127
+ type: "reasoning-start",
3128
+ id: reasoningId
3129
+ });
3130
+ reasoningStarted.set(idx, true);
3131
+ }
2824
3132
  controller.enqueue({
2825
3133
  type: "reasoning-delta",
2826
3134
  id: reasoningId,
@@ -2849,6 +3157,13 @@ ${plan}
2849
3157
  });
2850
3158
  }
2851
3159
  }
3160
+ if (!KNOWN_DELTA_TYPES.has(delta.type)) {
3161
+ log.debug("unrecognized content_block_delta type", {
3162
+ type: delta.type,
3163
+ idx,
3164
+ keys: Object.keys(delta)
3165
+ });
3166
+ }
2852
3167
  }
2853
3168
  if (msg.type === "content_block_stop" && msg.index !== void 0) {
2854
3169
  const idx = msg.index;
@@ -2946,6 +3261,42 @@ ${plan}
2946
3261
  if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
2947
3262
  lastStopReason = msg.message.stop_reason;
2948
3263
  }
3264
+ if (msg.type === "assistant" && msg.message?.content && gotPartialEvents) {
3265
+ const thinkingBlocks = msg.message.content.filter(
3266
+ (b) => b.type === "thinking"
3267
+ );
3268
+ if (thinkingBlocks.length > 0) {
3269
+ log.info("assistant message thinking blocks", {
3270
+ count: thinkingBlocks.length,
3271
+ hasText: thinkingBlocks.some(
3272
+ (b) => typeof b.thinking === "string" && b.thinking.length > 0
3273
+ ),
3274
+ hadStreamThinking: hadThinkingTextFromStream
3275
+ });
3276
+ if (!hadThinkingTextFromStream) {
3277
+ for (const block of thinkingBlocks) {
3278
+ if (block.thinking && block.thinking.length > 0) {
3279
+ noteReasoning();
3280
+ hadThinkingTextFromStream = true;
3281
+ const thinkingId = generateId();
3282
+ controller.enqueue({
3283
+ type: "reasoning-start",
3284
+ id: thinkingId
3285
+ });
3286
+ controller.enqueue({
3287
+ type: "reasoning-delta",
3288
+ id: thinkingId,
3289
+ delta: block.thinking
3290
+ });
3291
+ controller.enqueue({
3292
+ type: "reasoning-end",
3293
+ id: thinkingId
3294
+ });
3295
+ }
3296
+ }
3297
+ }
3298
+ }
3299
+ }
2949
3300
  if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
2950
3301
  const hasText = msg.message.content.some(
2951
3302
  (b) => b.type === "text" && b.text
@@ -3234,7 +3585,10 @@ ${plan}
3234
3585
  finishReason: toFinishReason("stop"),
3235
3586
  usage: toUsage(msg.usage),
3236
3587
  providerMetadata: {
3237
- "claude-code": resultMeta,
3588
+ "claude-code": {
3589
+ ...resultMeta,
3590
+ ...compactionMode ? { compactionModel: effectiveModelId } : {}
3591
+ },
3238
3592
  ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
3239
3593
  anthropic: {
3240
3594
  cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
@@ -3275,7 +3629,10 @@ ${plan}
3275
3629
  finishReason: toFinishReason("stop"),
3276
3630
  usage: toUsage(),
3277
3631
  providerMetadata: {
3278
- "claude-code": resultMeta
3632
+ "claude-code": {
3633
+ ...resultMeta,
3634
+ ...compactionMode ? { compactionModel: effectiveModelId } : {}
3635
+ }
3279
3636
  }
3280
3637
  });
3281
3638
  try {
@@ -3384,7 +3741,7 @@ ${plan}
3384
3741
  });
3385
3742
  resolvePendingProxyCallById(call.toolCallId, result);
3386
3743
  } else {
3387
- log.warn(
3744
+ log.notice(
3388
3745
  "pending proxy call had no matching tool-result; rejecting as orphan",
3389
3746
  {
3390
3747
  sessionKey: sk,
@@ -3875,7 +4232,8 @@ function createClaudeCode(settings = {}) {
3875
4232
  hotReloadMcp: settings.hotReloadMcp ?? true,
3876
4233
  proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
3877
4234
  multiStepContinuation: settings.multiStepContinuation ?? true,
3878
- autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart"
4235
+ autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
4236
+ compactionModel: settings.compactionModel
3879
4237
  });
3880
4238
  };
3881
4239
  const provider = function(modelId) {
@@ -4069,6 +4427,27 @@ var server = async (input) => {
4069
4427
  provider: {
4070
4428
  id: PROVIDER_ID2,
4071
4429
  models: async (provider) => defaultModelsForProvider(provider.models)
4430
+ },
4431
+ // Inject opencode's agent name into providerOptions so the language
4432
+ // model can distinguish /compact (and title) calls from normal turns.
4433
+ // Without this, every no-tools call looks like a title request and
4434
+ // gets short-circuited to a synthetic stub.
4435
+ "chat.params": async (input2, output) => {
4436
+ const providerID = input2.model?.providerID ?? input2.provider?.info?.id;
4437
+ log.debug("chat.params hook fired", {
4438
+ agent: input2.agent,
4439
+ providerID,
4440
+ sessionID: input2.sessionID
4441
+ });
4442
+ if (typeof providerID !== "string") return;
4443
+ if (providerID !== PROVIDER_ID2 && !providerID.startsWith(`${PROVIDER_ID2}-`)) return;
4444
+ if (!input2.agent) return;
4445
+ output.options ??= {};
4446
+ output.options.opencodeAgent = input2.agent;
4447
+ log.debug("chat.params tagged providerOptions", {
4448
+ agent: input2.agent,
4449
+ providerID
4450
+ });
4072
4451
  }
4073
4452
  };
4074
4453
  };