@khalilgharbaoui/opencode-claude-code-plugin 0.59.0 → 0.60.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.d.ts CHANGED
@@ -1511,6 +1511,13 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
1511
1511
  * claim taken: a choice must never land on the wrong child (h #g227).
1512
1512
  */
1513
1513
  private resolveDispatchChoice;
1514
+ /**
1515
+ * The model the parent named for THIS session in its own `task` call, or
1516
+ * undefined (h #g254). The same two lookups as `resolveDispatchChoice`, and
1517
+ * the same reason for the order: a held record first, and the parent fetched
1518
+ * only for a child whose agent and first message match a recorded request.
1519
+ */
1520
+ private resolveExplicitTaskModel;
1514
1521
  /**
1515
1522
  * Whether the ExitPlanMode approval bridge is live for this turn: the
1516
1523
  * operator opted in AND opencode's registry actually has the `question`
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ var V2_TOOLS = {
55
55
  name: "subagent",
56
56
  input: (input) => defined({
57
57
  agent: input.subagent_type ?? input.agent,
58
+ model: input.model,
58
59
  description: input.description,
59
60
  prompt: input.prompt,
60
61
  sessionID: input.task_id ?? input.sessionID,
@@ -3252,17 +3253,20 @@ function dispatchTasksFromCalls(calls, batchTasks) {
3252
3253
  const out = [];
3253
3254
  for (const call of calls) {
3254
3255
  if (call.toolName === "task") {
3255
- out.push(taskFromInput(call.toolCallId, null, call.input));
3256
+ if (!namesOwnModel(call.input)) out.push(taskFromInput(call.toolCallId, null, call.input));
3256
3257
  continue;
3257
3258
  }
3258
3259
  if (call.toolName === "task_batch") {
3259
3260
  for (const [index, task] of batchTasks(call.input).entries()) {
3260
- out.push(taskFromInput(call.toolCallId, index, task));
3261
+ if (!namesOwnModel(task)) out.push(taskFromInput(call.toolCallId, index, task));
3261
3262
  }
3262
3263
  }
3263
3264
  }
3264
3265
  return out;
3265
3266
  }
3267
+ function namesOwnModel(input) {
3268
+ return typeof input.model === "string" && input.model.trim() !== "";
3269
+ }
3266
3270
  function taskFromInput(callId, batchIndex, input) {
3267
3271
  return {
3268
3272
  callId,
@@ -4718,6 +4722,13 @@ var TASK_INPUT_PROPERTIES = {
4718
4722
  type: "string",
4719
4723
  description: "The type of specialized agent to use for this task"
4720
4724
  },
4725
+ // After the agent type, where opencode 2's own `subagent` tool puts it
4726
+ // (after `prompt`): first in the list it was the field the model read first.
4727
+ // Stripped on a host whose `task` has no such field (`applyTaskModelSupport`).
4728
+ model: {
4729
+ type: "string",
4730
+ description: "NEVER set this unless the user explicitly asked for a particular model or variant for this subagent. The exact opencode model reference, providerID/modelID, or providerID/modelID#variant to pick a variant. Use only an id the user gave you or one this conversation already used; never guess one. Omit it and opencode picks the model as usual."
4731
+ },
4721
4732
  task_id: {
4722
4733
  type: "string",
4723
4734
  description: "Set this only if you mean to resume a previous task: pass the prior task_id to continue the same subagent session instead of creating a fresh one."
@@ -4740,6 +4751,64 @@ var TASK_INPUT_PROPERTIES = {
4740
4751
  var TASK_INPUT_PROPERTIES_NO_BACKGROUND = Object.fromEntries(
4741
4752
  Object.entries(TASK_INPUT_PROPERTIES).filter(([name]) => name !== "background")
4742
4753
  );
4754
+ function liveTaskSupportsModel(taskParameters, dialect = "v1") {
4755
+ if (dialect === "v2") return true;
4756
+ const properties = taskParameters?.properties;
4757
+ if (!properties || typeof properties !== "object") return false;
4758
+ return "model" in properties;
4759
+ }
4760
+ function applyTaskModelSupport(tools, supported) {
4761
+ if (supported) return tools;
4762
+ const withoutModel = (schema) => {
4763
+ const properties = schema?.properties;
4764
+ if (!properties || typeof properties !== "object") return schema;
4765
+ return {
4766
+ ...schema,
4767
+ properties: Object.fromEntries(
4768
+ Object.entries(properties).filter(([name]) => name !== "model")
4769
+ )
4770
+ };
4771
+ };
4772
+ return tools.map((tool) => {
4773
+ if (tool.name === "task") return { ...tool, inputSchema: withoutModel(tool.inputSchema) };
4774
+ if (tool.name !== TASK_BATCH_TOOL_NAME) return tool;
4775
+ const properties = tool.inputSchema.properties;
4776
+ const tasks = properties?.tasks;
4777
+ if (!tasks?.items || typeof tasks.items !== "object") return tool;
4778
+ return {
4779
+ ...tool,
4780
+ inputSchema: {
4781
+ ...tool.inputSchema,
4782
+ properties: {
4783
+ ...properties,
4784
+ tasks: { ...tasks, items: withoutModel(tasks.items) }
4785
+ }
4786
+ }
4787
+ };
4788
+ });
4789
+ }
4790
+ function taskSchemaOffersModel(def) {
4791
+ const properties = def.inputSchema.properties;
4792
+ const schema = def.name === TASK_BATCH_TOOL_NAME ? properties?.tasks?.items : def.inputSchema;
4793
+ const fields = schema?.properties;
4794
+ return !!fields && typeof fields === "object" && "model" in fields;
4795
+ }
4796
+ function taskModelInputError(def, input, guard) {
4797
+ const tasks = def.name === TASK_BATCH_TOOL_NAME ? taskBatchTasks(input) : [input];
4798
+ const offered = taskSchemaOffersModel(def);
4799
+ for (const task of tasks) {
4800
+ if (task.model === void 0) continue;
4801
+ if (!offered) {
4802
+ return "This opencode's task tool takes no per-call `model` (opencode 1.x has none), so no tasks were started. Omit `model`.";
4803
+ }
4804
+ if (typeof task.model !== "string") {
4805
+ return "`model` must be a string, providerID/modelID or providerID/modelID#variant, so no tasks were started.";
4806
+ }
4807
+ const refusal = guard?.(task.model);
4808
+ if (refusal) return refusal;
4809
+ }
4810
+ return null;
4811
+ }
4743
4812
  var DEFAULT_PROXY_TOOLS = [
4744
4813
  {
4745
4814
  name: "bash",
@@ -5032,7 +5101,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
5032
5101
  const params = request.params ?? {};
5033
5102
  const toolName = String(params.name ?? "");
5034
5103
  const input = params.arguments ?? {};
5035
- if (!tools.some((t) => t.name === toolName)) {
5104
+ const tool = tools.find((t) => t.name === toolName);
5105
+ if (!tool) {
5036
5106
  writeJson(res, {
5037
5107
  jsonrpc: "2.0",
5038
5108
  id: requestId,
@@ -5050,6 +5120,13 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
5050
5120
  return;
5051
5121
  }
5052
5122
  }
5123
+ if (toolName === "task" || toolName === TASK_BATCH_TOOL_NAME) {
5124
+ const problem = taskModelInputError(tool, input, options.taskModelGuard);
5125
+ if (problem) {
5126
+ writeToolCallResult(res, requestId, { kind: "error", message: problem });
5127
+ return;
5128
+ }
5129
+ }
5053
5130
  const interceptor = interceptors?.get(toolName);
5054
5131
  if (interceptor) {
5055
5132
  let intercepted;
@@ -5808,6 +5885,112 @@ function clearLedger(sessionId) {
5808
5885
  ledgers.delete(sessionId);
5809
5886
  }
5810
5887
 
5888
+ // src/task-model.ts
5889
+ var TASK_MODEL_REFERENCE = /^([^/#]+)\/([^#]+)(?:#([^#]+))?$/;
5890
+ function parseTaskModelReference(value) {
5891
+ if (typeof value !== "string") return null;
5892
+ const match = TASK_MODEL_REFERENCE.exec(value.trim());
5893
+ if (!match) return null;
5894
+ const [, providerID, modelID, variant] = match;
5895
+ return variant ? { providerID, modelID, variant } : { providerID, modelID };
5896
+ }
5897
+ function taskModelAccountRefusal(input) {
5898
+ if (!input.groups || input.crossGroup) return null;
5899
+ const reference = parseTaskModelReference(input.model);
5900
+ if (!reference) return null;
5901
+ const target = input.providers.get(reference.providerID);
5902
+ if (target === void 0) return null;
5903
+ if (accountsShareGroup(input.sourceAccount, target, input.groups)) return null;
5904
+ log.notice("task model refused: its account is in another account group", {
5905
+ account: target,
5906
+ reason: "another-account-group"
5907
+ });
5908
+ return `The model "${input.model}" runs on the Claude account "${target}", which is outside this conversation's account group, so no tasks were started. A subagent is handed task text that can quote this conversation, and accountGroups keeps that text inside one group. Omit \`model\`, or name a model on an account in this conversation's group.`;
5909
+ }
5910
+ var CLAIM_TTL_MS2 = 15 * 6e4;
5911
+ var MAX_CLAIMS2 = 512;
5912
+ var MAX_SESSION_MODELS = 512;
5913
+ var claims2 = [];
5914
+ var sessionModels = /* @__PURE__ */ new Map();
5915
+ function pruneClaims2(now) {
5916
+ for (let i = claims2.length - 1; i >= 0; i--) {
5917
+ if (now - claims2[i].createdAt > CLAIM_TTL_MS2) claims2.splice(i, 1);
5918
+ }
5919
+ while (claims2.length > MAX_CLAIMS2) claims2.shift();
5920
+ }
5921
+ function holdSessionModel(sessionId, model) {
5922
+ sessionModels.delete(sessionId);
5923
+ sessionModels.set(sessionId, model);
5924
+ while (sessionModels.size > MAX_SESSION_MODELS) {
5925
+ const oldest = sessionModels.keys().next().value;
5926
+ if (oldest === void 0) break;
5927
+ sessionModels.delete(oldest);
5928
+ }
5929
+ }
5930
+ function noteExplicitTaskModels(parentSessionId, tasks, now = Date.now()) {
5931
+ let noted = 0;
5932
+ for (const task of tasks) {
5933
+ const reference = parseTaskModelReference(task.model);
5934
+ if (!reference) continue;
5935
+ const resumed = typeof task.task_id === "string" ? task.task_id.trim() : "";
5936
+ if (resumed) {
5937
+ holdSessionModel(resumed, reference);
5938
+ noted++;
5939
+ continue;
5940
+ }
5941
+ if (!parentSessionId || parentSessionId === "default") continue;
5942
+ pruneClaims2(now);
5943
+ claims2.push({
5944
+ parentSessionId,
5945
+ agent: typeof task.subagent_type === "string" ? task.subagent_type : "general",
5946
+ prompt: typeof task.prompt === "string" ? task.prompt : "",
5947
+ model: reference,
5948
+ createdAt: now
5949
+ });
5950
+ noted++;
5951
+ }
5952
+ return noted;
5953
+ }
5954
+ function explicitTaskModelFor(sessionId) {
5955
+ return sessionModels.get(sessionId);
5956
+ }
5957
+ function hasExplicitTaskModelCandidate(agent, prompt, now = Date.now()) {
5958
+ if (!agent || prompt === void 0 || claims2.length === 0) return false;
5959
+ pruneClaims2(now);
5960
+ return claims2.some(
5961
+ (claim) => claim.agent === agent && promptMatchesClaim(claim.prompt, prompt)
5962
+ );
5963
+ }
5964
+ function claimExplicitTaskModel(input) {
5965
+ const held = sessionModels.get(input.sessionId);
5966
+ if (held) return held;
5967
+ if (!input.agent || input.prompt === void 0 || !input.parentSessionId) return void 0;
5968
+ pruneClaims2(input.now ?? Date.now());
5969
+ const belongsHere = (claim2) => claim2.parentSessionId === input.parentSessionId && claim2.agent === input.agent;
5970
+ const childPrompt = input.prompt;
5971
+ let index = claims2.findIndex((claim2) => belongsHere(claim2) && claim2.prompt === childPrompt);
5972
+ if (index === -1) {
5973
+ index = claims2.findIndex(
5974
+ (claim2) => belongsHere(claim2) && promptMatchesClaim(claim2.prompt, childPrompt)
5975
+ );
5976
+ }
5977
+ if (index === -1) return void 0;
5978
+ const [claim] = claims2.splice(index, 1);
5979
+ holdSessionModel(input.sessionId, claim.model);
5980
+ log.info("explicit task model claimed by its child session", {
5981
+ sessionId: input.sessionId,
5982
+ agent: input.agent,
5983
+ model: claim.model.modelID
5984
+ });
5985
+ return claim.model;
5986
+ }
5987
+ function explicitTaskModelApplies(record, modelId) {
5988
+ return record !== void 0 && record.modelID === modelId;
5989
+ }
5990
+ function forgetExplicitTaskModel(sessionId) {
5991
+ sessionModels.delete(sessionId);
5992
+ }
5993
+
5811
5994
  // src/compression-store.ts
5812
5995
  var MAX_COMPRESSION_ENTRIES = 32;
5813
5996
  var compressions = /* @__PURE__ */ new Map();
@@ -7820,6 +8003,7 @@ function deleteActiveProcessesForSession(sessionID) {
7820
8003
  for (const key of released) clearBackgroundTasks(key);
7821
8004
  forgetSessionChoice(sessionID);
7822
8005
  forgetDispatchChoicesForSession(sessionID);
8006
+ forgetExplicitTaskModel(sessionID);
7823
8007
  return released;
7824
8008
  }
7825
8009
  var STOPPED_TURN_MESSAGE = "opencode stopped the turn while it was running its proxy tool calls";
@@ -8907,6 +9091,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
8907
9091
  "subagent dispatch answer was not recognised; dispatching with the defaults",
8908
9092
  "subagent dispatch answer was not recognised; keeping the default",
8909
9093
  "subagent dispatch form was dismissed; dispatching with the defaults",
9094
+ "task model refused: its account is in another account group",
8910
9095
  "task_batch resolving with child results missing",
8911
9096
  "the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
8912
9097
  "the interactive turn did not take a forwarded user message",
@@ -15001,7 +15186,7 @@ async function resolvePlanModeQuestion(config, compactionMode, loadLiveToolInfo)
15001
15186
  }
15002
15187
  return active;
15003
15188
  }
15004
- async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
15189
+ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress, callerSessionId, sourceAccount) {
15005
15190
  const timeoutOverrides = config.proxyToolTimeoutMs;
15006
15191
  const interceptors = /* @__PURE__ */ new Map();
15007
15192
  if (tools.some((t) => t.name === TASK_STATUS_TOOL_NAME)) {
@@ -15031,8 +15216,25 @@ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCom
15031
15216
  }
15032
15217
  const cap = config.maxBackgroundSubagents;
15033
15218
  const admit = cap !== void 0 && tools.some((t) => t.name === "task" || t.name === TASK_BATCH_TOOL_NAME) ? (toolName, input) => admitBackgroundDispatch(toolName, input, cap) : void 0;
15034
- const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors, { admit });
15219
+ const accountProviders = accountProviderMap(config.failoverAccounts);
15220
+ const groups = config.accountGroups ?? null;
15221
+ const caller = normalizeAccountName(sourceAccount ?? config.account ?? DEFAULT_ACCOUNT);
15222
+ const taskModelGuard = groups && accountProviders.size > 0 ? (model) => taskModelAccountRefusal({
15223
+ model,
15224
+ sourceAccount: caller,
15225
+ providers: accountProviders,
15226
+ groups,
15227
+ crossGroup: config.subagentDispatchCrossGroup === true
15228
+ }) : void 0;
15229
+ const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors, {
15230
+ admit,
15231
+ taskModelGuard
15232
+ });
15035
15233
  srv.calls.on("call", (call) => {
15234
+ if (call.toolName === "task") noteExplicitTaskModels(callerSessionId, [call.input]);
15235
+ else if (call.toolName === TASK_BATCH_TOOL_NAME) {
15236
+ noteExplicitTaskModels(callerSessionId, taskBatchTasks(call.input));
15237
+ }
15036
15238
  queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
15037
15239
  });
15038
15240
  return srv;
@@ -17000,6 +17202,26 @@ var ClaudeCodeLanguageModel = class {
17000
17202
  parentSessionId: await fetchSessionParentId(affinity)
17001
17203
  });
17002
17204
  }
17205
+ /**
17206
+ * The model the parent named for THIS session in its own `task` call, or
17207
+ * undefined (h #g254). The same two lookups as `resolveDispatchChoice`, and
17208
+ * the same reason for the order: a held record first, and the parent fetched
17209
+ * only for a child whose agent and first message match a recorded request.
17210
+ */
17211
+ async resolveExplicitTaskModel(options, affinity) {
17212
+ if (affinity === "default") return void 0;
17213
+ const held = explicitTaskModelFor(affinity);
17214
+ if (held) return held;
17215
+ const agent = this.getOpencodeAgent(options);
17216
+ const prompt = firstUserText(options.prompt);
17217
+ if (!hasExplicitTaskModelCandidate(agent, prompt)) return void 0;
17218
+ return claimExplicitTaskModel({
17219
+ sessionId: affinity,
17220
+ agent,
17221
+ prompt,
17222
+ parentSessionId: await fetchSessionParentId(affinity)
17223
+ });
17224
+ }
17003
17225
  /**
17004
17226
  * Whether the ExitPlanMode approval bridge is live for this turn: the
17005
17227
  * operator opted in AND opencode's registry actually has the `question`
@@ -17173,13 +17395,14 @@ var ClaudeCodeLanguageModel = class {
17173
17395
  * Create a proxy MCP server for a single active Claude process/session.
17174
17396
  * The process lifecycle owns the server lifecycle via session-manager.
17175
17397
  */
17176
- async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId) {
17398
+ async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress, callerSessionId, sourceAccount) {
17177
17399
  return ensureProxyServer(
17178
17400
  this.config,
17179
17401
  tools,
17180
17402
  sessionKeyForCalls,
17181
17403
  interceptCompress,
17182
- callerSessionId
17404
+ callerSessionId,
17405
+ sourceAccount
17183
17406
  );
17184
17407
  }
17185
17408
  /**
@@ -17570,17 +17793,19 @@ var ClaudeCodeLanguageModel = class {
17570
17793
  const affinity = this.sessionAffinity(options);
17571
17794
  const cwd = await resolveSpawnCwdForSession(this.config.cwd, affinity);
17572
17795
  const compactionMode = this.isCompactionCall(options);
17573
- const dispatchChoice = compactionMode ? void 0 : await this.resolveDispatchChoice(options, affinity);
17796
+ const explicitTaskModel = compactionMode ? void 0 : await this.resolveExplicitTaskModel(options, affinity);
17797
+ const explicitModelApplies = explicitTaskModelApplies(explicitTaskModel, this.modelId);
17798
+ const dispatchChoice = compactionMode || explicitModelApplies ? void 0 : await this.resolveDispatchChoice(options, affinity);
17574
17799
  const dispatchModelId = dispatchChoice?.model ? qualifyModelName(dispatchChoice.model, this.modelId) : null;
17575
- const effectiveModelId = compactionMode ? this.resolveCompactionModel() : attempt?.modelOverride ?? dispatchModelId ?? resolveAgentModel(
17800
+ const effectiveModelId = compactionMode ? this.resolveCompactionModel() : attempt?.modelOverride ?? dispatchModelId ?? (explicitModelApplies ? this.modelId : resolveAgentModel(
17576
17801
  this.getOpencodeAgent(options),
17577
17802
  this.modelId
17578
- );
17803
+ ));
17579
17804
  if (attempt) attempt.modelId = effectiveModelId;
17580
- const reasoningEffort = compactionMode ? void 0 : dispatchChoice?.effort ?? resolveAgentEffort(
17805
+ const reasoningEffort = compactionMode ? void 0 : dispatchChoice?.effort ?? (explicitModelApplies && explicitTaskModel?.variant ? this.getReasoningEffort(options.providerOptions) : resolveAgentEffort(
17581
17806
  this.getOpencodeAgent(options),
17582
17807
  this.getReasoningEffort(options.providerOptions)
17583
- );
17808
+ ));
17584
17809
  const promptCacheTtl = compactionMode ? void 0 : resolveAgentCacheTtl(this.getOpencodeAgent(options));
17585
17810
  const context = [
17586
17811
  this.config.provider,
@@ -18610,6 +18835,16 @@ var ClaudeCodeLanguageModel = class {
18610
18835
  backgroundSubagentsSupported,
18611
18836
  hostDialect
18612
18837
  );
18838
+ const taskModelSupported = liveTaskSupportsModel(
18839
+ liveToolInfo.taskParameters,
18840
+ hostDialect
18841
+ );
18842
+ enrichedProxy = applyTaskModelSupport(enrichedProxy, taskModelSupported);
18843
+ log.info("task model gate", {
18844
+ supported: taskModelSupported,
18845
+ registryResolved: liveToolInfo.resolved,
18846
+ hostApi: hostDialect
18847
+ });
18613
18848
  recordBackgroundSubagentGate({
18614
18849
  supported: backgroundSubagentsSupported,
18615
18850
  hostApi: hostDialect,
@@ -18664,7 +18899,8 @@ var ClaudeCodeLanguageModel = class {
18664
18899
  combinedProxyTools,
18665
18900
  sk,
18666
18901
  pluginCompressEnabled,
18667
- affinity
18902
+ affinity,
18903
+ sourceAccount
18668
18904
  );
18669
18905
  }
18670
18906
  const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;