@khalilgharbaoui/opencode-claude-code-plugin 0.62.0 → 0.63.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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/claude-code-language-model.ts
2
- import { join as join19 } from "path";
2
+ import { join as join20 } from "path";
3
3
 
4
4
  // src/ids.ts
5
5
  import { randomInt } from "crypto";
@@ -2045,6 +2045,10 @@ function clearAccountFailoverQuestions(sessionKey2) {
2045
2045
  if (key.startsWith(prefix)) pendingQuestions2.delete(key);
2046
2046
  }
2047
2047
  }
2048
+ function hasAccountFailoverQuestions(sessionKey2) {
2049
+ const prefix = `${sessionKey2}${KEY_SEPARATOR2}`;
2050
+ return [...pendingQuestions2.keys()].some((key) => key.startsWith(prefix));
2051
+ }
2048
2052
  function describeReset(resetsAt) {
2049
2053
  return formatResetsAt(resetsAt);
2050
2054
  }
@@ -3164,6 +3168,10 @@ function clearSubagentDispatchQuestions(sessionKey2) {
3164
3168
  if (key.startsWith(prefix)) pending.delete(key);
3165
3169
  }
3166
3170
  }
3171
+ function hasPendingSubagentDispatch(sessionKey2) {
3172
+ const prefix = `${sessionKey2}${KEY_SEPARATOR3}`;
3173
+ return [...pending.keys()].some((key) => key.startsWith(prefix));
3174
+ }
3167
3175
  function isSubagentDispatchActive(input) {
3168
3176
  if (input.compactionMode) return false;
3169
3177
  if (input.childSession) return false;
@@ -5833,6 +5841,9 @@ function isPendingProxyCallChannelClosed(call) {
5833
5841
  function findPendingProxyCall(toolCallId) {
5834
5842
  return pendingByCallId.get(toolCallId);
5835
5843
  }
5844
+ function hasAnyPendingProxyCall() {
5845
+ return pendingByCallId.size > 0;
5846
+ }
5836
5847
  function getPendingProxyCalls(sessionKey2) {
5837
5848
  const s = callIdsBySession.get(sessionKey2);
5838
5849
  if (!s || s.size === 0) return [];
@@ -6074,6 +6085,10 @@ function noteExplicitTaskModels(parentSessionId, tasks, now = Date.now()) {
6074
6085
  }
6075
6086
  return noted;
6076
6087
  }
6088
+ function hasUnclaimedExplicitTaskModels(now = Date.now()) {
6089
+ pruneClaims2(now);
6090
+ return claims2.length > 0;
6091
+ }
6077
6092
  function explicitTaskModelFor(sessionId) {
6078
6093
  return sessionModels.get(sessionId);
6079
6094
  }
@@ -7856,6 +7871,77 @@ async function requestSideQuestion(activeProcess, question, options) {
7856
7871
  });
7857
7872
  }
7858
7873
 
7874
+ // src/session-event-relay.ts
7875
+ var RELAY_KEY = /* @__PURE__ */ Symbol.for("@khalilgharbaoui/opencode-claude-code-plugin/session-event-relay");
7876
+ function relayState() {
7877
+ const holder = globalThis;
7878
+ let state = holder[RELAY_KEY];
7879
+ if (!state) {
7880
+ state = { members: /* @__PURE__ */ new Set(), followers: 0 };
7881
+ holder[RELAY_KEY] = state;
7882
+ }
7883
+ return state;
7884
+ }
7885
+ function safely(action) {
7886
+ try {
7887
+ action();
7888
+ } catch {
7889
+ }
7890
+ }
7891
+ var ownMember;
7892
+ var ownFollowers = 0;
7893
+ var ownServed = false;
7894
+ function join10() {
7895
+ if (!ownMember) return;
7896
+ const state = relayState();
7897
+ if (state.members.has(ownMember)) return;
7898
+ state.members.add(ownMember);
7899
+ if (state.followers > 0) safely(() => ownMember?.feedOpened());
7900
+ }
7901
+ function leave() {
7902
+ if (!ownMember) return;
7903
+ if (relayState().members.delete(ownMember)) safely(() => ownMember?.feedClosed());
7904
+ }
7905
+ function defineOwnRelayMember(member) {
7906
+ ownMember = member;
7907
+ if (ownServed) join10();
7908
+ }
7909
+ function noteRelayMemberServing() {
7910
+ ownServed = true;
7911
+ join10();
7912
+ }
7913
+ function relayMemberHasServed() {
7914
+ return ownServed;
7915
+ }
7916
+ function leaveSessionEventRelay() {
7917
+ leave();
7918
+ }
7919
+ function openRelayFollower() {
7920
+ const state = relayState();
7921
+ join10();
7922
+ ownFollowers += 1;
7923
+ state.followers += 1;
7924
+ if (state.followers === 1) {
7925
+ for (const member of state.members) safely(() => member.feedOpened());
7926
+ }
7927
+ let closed = false;
7928
+ return () => {
7929
+ if (closed) return;
7930
+ closed = true;
7931
+ ownFollowers = Math.max(0, ownFollowers - 1);
7932
+ state.followers = Math.max(0, state.followers - 1);
7933
+ if (state.followers === 0) {
7934
+ for (const member of state.members) safely(() => member.feedClosed());
7935
+ }
7936
+ if (ownFollowers === 0 && !ownServed) leave();
7937
+ };
7938
+ }
7939
+ function relaySessionEvent(event) {
7940
+ const members = relayState().members;
7941
+ if (ownMember && !members.has(ownMember)) safely(() => ownMember?.handle(event));
7942
+ for (const member of [...members]) safely(() => member.handle(event));
7943
+ }
7944
+
7859
7945
  // src/session-manager.ts
7860
7946
  function findActiveProcessBySessionId(sessionID) {
7861
7947
  let found;
@@ -8082,6 +8168,7 @@ function processBelongsToAnotherAccount(active, cliPath, configDir) {
8082
8168
  function setActiveProcess(key, ap) {
8083
8169
  cancelIdleProcessEviction(key);
8084
8170
  activeProcesses.set(key, ap);
8171
+ noteRelayMemberServing();
8085
8172
  }
8086
8173
  function noteInteractiveProcessExit(sessionKey2, ap, code) {
8087
8174
  if (activeProcesses.get(sessionKey2) !== ap) return;
@@ -8261,6 +8348,10 @@ function setClaudeSessionId(key, sessionId) {
8261
8348
  capClaudeSessions();
8262
8349
  }
8263
8350
  function deleteClaudeSessionId(key) {
8351
+ forgetClaudeSessionInMemory(key);
8352
+ forgetResumePoint(key);
8353
+ }
8354
+ function forgetClaudeSessionInMemory(key) {
8264
8355
  clearExitPlanModeQuestions(key);
8265
8356
  clearAccountFailoverQuestions(key);
8266
8357
  clearSubagentDispatchQuestions(key);
@@ -8269,7 +8360,52 @@ function deleteClaudeSessionId(key) {
8269
8360
  claudeSessions.delete(key);
8270
8361
  forgetForkFingerprint(key);
8271
8362
  forgetServedConversation(key);
8272
- forgetResumePoint(key);
8363
+ }
8364
+ function keyBelongsToSession(key, sessionID, ap) {
8365
+ return ap?.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
8366
+ }
8367
+ function keyHeldHere(key) {
8368
+ return claudeSessionIsWriting(key) || hasAccountFailoverQuestions(key) || hasPendingSubagentDispatch(key);
8369
+ }
8370
+ function sessionHeldHere(sessionID) {
8371
+ if (!sessionID || sessionID === "default") return false;
8372
+ for (const [key, ap] of activeProcesses) {
8373
+ if (keyBelongsToSession(key, sessionID, ap) && keyHeldHere(key)) return true;
8374
+ }
8375
+ for (const key of claudeSessions.keys()) {
8376
+ if (keyBelongsToSession(key, sessionID) && keyHeldHere(key)) return true;
8377
+ }
8378
+ return false;
8379
+ }
8380
+ function noWorkInFlightHere() {
8381
+ for (const ap of activeProcesses.values()) {
8382
+ if (isTurnInFlight(ap)) return false;
8383
+ }
8384
+ return !hasAnyPendingProxyCall() && !hasUnclaimedExplicitTaskModels();
8385
+ }
8386
+ function holdsNoClaudeState() {
8387
+ return activeProcesses.size === 0 && claudeSessions.size === 0 && !hasAnyPendingProxyCall();
8388
+ }
8389
+ async function releaseIdleForHandover(sessionID) {
8390
+ if (sessionID === "default") return [];
8391
+ const belongs = (key, ap) => sessionID === void 0 || keyBelongsToSession(key, sessionID, ap);
8392
+ const released = [];
8393
+ const exits = [];
8394
+ for (const [key, ap] of [...activeProcesses]) {
8395
+ if (!belongs(key, ap) || keyHeldHere(key)) continue;
8396
+ exits.push(deleteActiveProcessAndWait(key));
8397
+ released.push(key);
8398
+ }
8399
+ await Promise.all(exits);
8400
+ for (const key of [...claudeSessions.keys()]) {
8401
+ if (!belongs(key) || keyHeldHere(key)) continue;
8402
+ forgetClaudeSessionInMemory(key);
8403
+ if (!released.includes(key)) released.push(key);
8404
+ }
8405
+ if (released.length > 0) {
8406
+ log.info("handed claude sessions to another copy of the plugin", { sessionID, released });
8407
+ }
8408
+ return released;
8273
8409
  }
8274
8410
  function transferClaudeSession(fromKey, toKey) {
8275
8411
  if (fromKey === toKey) return void 0;
@@ -8376,8 +8512,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
8376
8512
  settleTurn(ap);
8377
8513
  lineEmitter.emit("close");
8378
8514
  });
8379
- cancelIdleProcessEviction(sessionKey2);
8380
- activeProcesses.set(sessionKey2, ap);
8515
+ setActiveProcess(sessionKey2, ap);
8381
8516
  proc.on("error", (err) => {
8382
8517
  log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
8383
8518
  });
@@ -9101,6 +9236,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
9101
9236
  "agent maxTurns refused: not a positive whole number",
9102
9237
  "agent model override refused: unknown model",
9103
9238
  "agent prompt cache ttl refused: unknown value",
9239
+ "an older plugin build failed to release its claude sessions",
9104
9240
  "an opencode read deny rule could not be mapped onto Claude's own Read; that path stays readable by the CLI's own file tools",
9105
9241
  "auto-continuation stopped",
9106
9242
  "auto-continuing incomplete claude result",
@@ -9247,6 +9383,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
9247
9383
  "task_batch resolving with child results missing",
9248
9384
  "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",
9249
9385
  "the interactive turn did not take a forwarded user message",
9386
+ "the plugin build chosen for this turn could not take it; running it on this one",
9250
9387
  "this opencode process is running an older plugin build than the one on disk",
9251
9388
  "timed out pending proxy call",
9252
9389
  "turn abort listener threw",
@@ -9952,7 +10089,7 @@ function emptyMapping(enabled) {
9952
10089
  }
9953
10090
  function mapReadRules(rules, options = {}) {
9954
10091
  const deny = [];
9955
- const ask = [];
10092
+ const ask2 = [];
9956
10093
  const unmapped = [];
9957
10094
  const shadowedAllows = [];
9958
10095
  const seen = /* @__PURE__ */ new Set();
@@ -9960,7 +10097,7 @@ function mapReadRules(rules, options = {}) {
9960
10097
  rules.forEach((rule, index) => {
9961
10098
  if (rule.effect === "ask") {
9962
10099
  if (supersededBy(index, rule.pattern, () => true)) return;
9963
- if (!ask.includes(rule.pattern)) ask.push(rule.pattern);
10100
+ if (!ask2.includes(rule.pattern)) ask2.push(rule.pattern);
9964
10101
  return;
9965
10102
  }
9966
10103
  if (rule.effect !== "deny") return;
@@ -9981,7 +10118,7 @@ function mapReadRules(rules, options = {}) {
9981
10118
  if (!shadowedAllows.includes(later.pattern)) shadowedAllows.push(later.pattern);
9982
10119
  }
9983
10120
  });
9984
- return { deny, ask, unmapped, shadowedAllows };
10121
+ return { deny, ask: ask2, unmapped, shadowedAllows };
9985
10122
  }
9986
10123
  function resolveReadRuleMapping(input) {
9987
10124
  if (input.enabled === false) return emptyMapping(false);
@@ -10115,20 +10252,20 @@ async function fetchPlanUsage(cliPath, options = {}) {
10115
10252
  // src/turn-history.ts
10116
10253
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync6, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
10117
10254
  import { homedir as homedir11 } from "os";
10118
- import { join as join11 } from "path";
10255
+ import { join as join12 } from "path";
10119
10256
 
10120
10257
  // src/turn-progress-store.ts
10121
10258
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
10122
10259
  import { homedir as homedir10 } from "os";
10123
- import { join as join10 } from "path";
10260
+ import { join as join11 } from "path";
10124
10261
  var MAX_TURN_PROGRESS = 64;
10125
10262
  var TURN_PROGRESS_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
10126
10263
  var TAG = /^[0-9a-f]{8}$/;
10127
10264
  var pathOverride4 = null;
10128
10265
  function turnProgressStorePath() {
10129
10266
  if (pathOverride4) return pathOverride4;
10130
- const stateRoot = process.env.XDG_STATE_HOME || join10(homedir10(), ".local", "state");
10131
- return join10(stateRoot, "opencode-claude-code-plugin", "turn-progress.json");
10267
+ const stateRoot = process.env.XDG_STATE_HOME || join11(homedir10(), ".local", "state");
10268
+ return join11(stateRoot, "opencode-claude-code-plugin", "turn-progress.json");
10132
10269
  }
10133
10270
  function isProgress(value) {
10134
10271
  const record = value;
@@ -10158,7 +10295,7 @@ function recordTurnProgress(tag, progress, now = Date.now()) {
10158
10295
  merged.set(tag, { ...record, out: Math.round(record.out) });
10159
10296
  const kept = [...merged.entries()].slice(-MAX_TURN_PROGRESS);
10160
10297
  try {
10161
- mkdirSync6(join10(file, ".."), { recursive: true, mode: 448 });
10298
+ mkdirSync6(join11(file, ".."), { recursive: true, mode: 448 });
10162
10299
  const tmp = `${file}.${process.pid}.tmp`;
10163
10300
  writeFileSync6(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
10164
10301
  renameSync5(tmp, file);
@@ -10176,8 +10313,8 @@ var SESSION_TAG = /^[0-9a-f]{8}$/;
10176
10313
  var pathOverride5 = null;
10177
10314
  function turnHistoryStorePath() {
10178
10315
  if (pathOverride5) return pathOverride5;
10179
- const stateRoot = process.env.XDG_STATE_HOME || join11(homedir11(), ".local", "state");
10180
- return join11(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
10316
+ const stateRoot = process.env.XDG_STATE_HOME || join12(homedir11(), ".local", "state");
10317
+ return join12(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
10181
10318
  }
10182
10319
  function turnHistoryKey(agent, model) {
10183
10320
  return `${agent || "main"}/${model}`;
@@ -10361,7 +10498,7 @@ function recordTurnSample(key, sample) {
10361
10498
  merged.set(key, samples);
10362
10499
  const kept = bounded2(merged);
10363
10500
  try {
10364
- mkdirSync7(join11(file, ".."), { recursive: true, mode: 448 });
10501
+ mkdirSync7(join12(file, ".."), { recursive: true, mode: 448 });
10365
10502
  const tmp = `${file}.${process.pid}.tmp`;
10366
10503
  writeFileSync7(tmp, JSON.stringify({ version: 1, turns: Object.fromEntries(kept) }), { mode: 384 });
10367
10504
  renameSync6(tmp, file);
@@ -10736,6 +10873,159 @@ import * as fs7 from "fs";
10736
10873
  import * as path10 from "path";
10737
10874
  import { fileURLToPath as fileURLToPath2 } from "url";
10738
10875
 
10876
+ // src/plugin-copies.ts
10877
+ var ROUTED_TURN_LIMIT_MS = 2 * 60 * 6e4;
10878
+ var COPIES_KEY = /* @__PURE__ */ Symbol.for("@khalilgharbaoui/opencode-claude-code-plugin/copies/v1");
10879
+ function copiesState() {
10880
+ const holder = globalThis;
10881
+ let state = holder[COPIES_KEY];
10882
+ if (!state) {
10883
+ state = { copies: [], active: void 0, switching: void 0, turns: /* @__PURE__ */ new Map() };
10884
+ holder[COPIES_KEY] = state;
10885
+ }
10886
+ return state;
10887
+ }
10888
+ function routedTurns(state, now) {
10889
+ for (const [token, turn] of state.turns) {
10890
+ if (now - turn.startedAt > ROUTED_TURN_LIMIT_MS) state.turns.delete(token);
10891
+ }
10892
+ return [...state.turns.values()];
10893
+ }
10894
+ function ask(question, fallback) {
10895
+ try {
10896
+ return question();
10897
+ } catch {
10898
+ return fallback;
10899
+ }
10900
+ }
10901
+ async function releaseQuietly(copy, sessionID) {
10902
+ try {
10903
+ await copy.release(sessionID);
10904
+ } catch (err) {
10905
+ log.warn("an older plugin build failed to release its claude sessions", {
10906
+ version: copy.version,
10907
+ error: err instanceof Error ? err.message : String(err)
10908
+ });
10909
+ }
10910
+ }
10911
+ function registerPluginCopy(copy) {
10912
+ const state = copiesState();
10913
+ if (state.copies.includes(copy)) return;
10914
+ state.copies.push(copy);
10915
+ state.active ??= copy;
10916
+ }
10917
+ function unregisterPluginCopy(copy) {
10918
+ const state = copiesState();
10919
+ const index = state.copies.indexOf(copy);
10920
+ if (index !== -1) state.copies.splice(index, 1);
10921
+ if (state.active === copy) state.active = void 0;
10922
+ }
10923
+ function isActivePluginCopy(copy) {
10924
+ return copiesState().active === copy;
10925
+ }
10926
+ function processLoadedBuild(build) {
10927
+ return copiesState().copies.some(
10928
+ (copy) => !copy.disposed && copy.version === build.version && copy.build.entryPath === build.entryPath && copy.build.mtimeMs === build.mtimeMs && copy.build.size === build.size
10929
+ );
10930
+ }
10931
+ function switchWhenQuiet(state, now) {
10932
+ if (state.switching) return;
10933
+ const newest = [...state.copies].reverse().find((copy) => !copy.disposed);
10934
+ if (!newest || newest === state.active) return;
10935
+ const others = state.copies.filter((copy) => copy !== newest);
10936
+ const busy = new Set(routedTurns(state, now).map((turn) => turn.copy));
10937
+ if (!others.every((copy) => !busy.has(copy) && ask(() => copy.quiet(), false))) return;
10938
+ const previous = state.active;
10939
+ state.active = newest;
10940
+ log.info("switched to the newest plugin build", {
10941
+ from: previous?.version,
10942
+ to: newest.version
10943
+ });
10944
+ state.switching = Promise.all(others.map((copy) => releaseQuietly(copy))).then(() => {
10945
+ state.switching = void 0;
10946
+ });
10947
+ }
10948
+ function servingCopy(state, sessionID, own) {
10949
+ if (sessionID) {
10950
+ const holder = state.copies.find((copy) => ask(() => copy.holds(sessionID), false));
10951
+ if (holder) return holder;
10952
+ }
10953
+ return state.active ?? own;
10954
+ }
10955
+ function followStream(source, end) {
10956
+ const reader = source.getReader();
10957
+ return new ReadableStream({
10958
+ async pull(controller) {
10959
+ try {
10960
+ const { done, value } = await reader.read();
10961
+ if (done) {
10962
+ end();
10963
+ controller.close();
10964
+ return;
10965
+ }
10966
+ controller.enqueue(value);
10967
+ } catch (err) {
10968
+ end();
10969
+ controller.error(err);
10970
+ }
10971
+ },
10972
+ async cancel(reason) {
10973
+ end();
10974
+ await reader.cancel(reason);
10975
+ }
10976
+ });
10977
+ }
10978
+ function untilTurnEnds(result, end) {
10979
+ const stream = result?.stream;
10980
+ if (!(stream instanceof ReadableStream)) {
10981
+ end();
10982
+ return result;
10983
+ }
10984
+ return { ...result, stream: followStream(stream, end) };
10985
+ }
10986
+ async function routeTurn(input) {
10987
+ const state = copiesState();
10988
+ if (state.copies.length === 0) return input.runHere();
10989
+ const now = input.now ?? Date.now;
10990
+ if (state.switching) await state.switching;
10991
+ switchWhenQuiet(state, now());
10992
+ if (state.switching) await state.switching;
10993
+ const sessionID = input.sessionID && input.sessionID !== "default" ? input.sessionID : void 0;
10994
+ const target = servingCopy(state, sessionID, input.own);
10995
+ const token = {};
10996
+ state.turns.set(token, { copy: target, sessionID, startedAt: now() });
10997
+ let ended = false;
10998
+ const end = () => {
10999
+ if (ended) return;
11000
+ ended = true;
11001
+ state.turns.delete(token);
11002
+ };
11003
+ try {
11004
+ if (sessionID) {
11005
+ await Promise.all(
11006
+ state.copies.filter((copy) => copy !== target).map((copy) => releaseQuietly(copy, sessionID))
11007
+ );
11008
+ }
11009
+ if (target === input.own) return untilTurnEnds(await input.runHere(), end);
11010
+ let model;
11011
+ try {
11012
+ model = await target.modelFor(input.turn);
11013
+ } catch (err) {
11014
+ log.warn("the plugin build chosen for this turn could not take it; running it on this one", {
11015
+ version: target.version,
11016
+ error: err instanceof Error ? err.message : String(err)
11017
+ });
11018
+ const turn = state.turns.get(token);
11019
+ if (turn) turn.copy = input.own;
11020
+ return untilTurnEnds(await input.runHere(), end);
11021
+ }
11022
+ return untilTurnEnds(await input.runOn(model), end);
11023
+ } catch (err) {
11024
+ end();
11025
+ throw err;
11026
+ }
11027
+ }
11028
+
10739
11029
  // src/startup-diagnostics.ts
10740
11030
  import { execFile as execFile3 } from "child_process";
10741
11031
  import * as fs6 from "fs";
@@ -11000,6 +11290,8 @@ function createStaleBuildWatch(io) {
11000
11290
  const warned = /* @__PURE__ */ new Set();
11001
11291
  let checkedAt;
11002
11292
  let cached2 = null;
11293
+ let cachedOnDisk;
11294
+ const loadedHere = (onDisk) => onDisk !== void 0 && io.loadedElsewhere?.(onDisk) === true;
11003
11295
  const read = () => {
11004
11296
  let onDisk;
11005
11297
  try {
@@ -11017,18 +11309,21 @@ function createStaleBuildWatch(io) {
11017
11309
  },
11018
11310
  check() {
11019
11311
  const now = io.now();
11020
- if (checkedAt !== void 0 && now - checkedAt < throttleMs) return cached2;
11312
+ if (checkedAt !== void 0 && now - checkedAt < throttleMs) {
11313
+ return cached2 && !loadedHere(cachedOnDisk) ? cached2 : null;
11314
+ }
11021
11315
  checkedAt = now;
11022
11316
  const status = read();
11023
- cached2 = status.stale;
11024
- if (status.stale && status.onDisk) {
11025
- const identity = `${status.stale.kind}:${status.onDisk.version}:${status.onDisk.mtimeMs}:${status.onDisk.size}`;
11317
+ cachedOnDisk = status.onDisk;
11318
+ cached2 = status.stale && loadedHere(status.onDisk) ? null : status.stale;
11319
+ if (cached2 && status.onDisk) {
11320
+ const identity = `${cached2.kind}:${status.onDisk.version}:${status.onDisk.mtimeMs}:${status.onDisk.size}`;
11026
11321
  if (!warned.has(identity)) {
11027
11322
  warned.add(identity);
11028
11323
  warn({
11029
- kind: status.stale.kind,
11030
- loadedVersion: status.stale.loadedVersion,
11031
- onDiskVersion: status.stale.onDiskVersion,
11324
+ kind: cached2.kind,
11325
+ loadedVersion: cached2.loadedVersion,
11326
+ onDiskVersion: cached2.onDiskVersion,
11032
11327
  path: io.loaded.entryPath
11033
11328
  });
11034
11329
  }
@@ -11087,7 +11382,8 @@ function staleBuildWatch() {
11087
11382
  watch = createStaleBuildWatch({
11088
11383
  loaded,
11089
11384
  readOnDisk: () => readOnDiskBuild(loaded.entryPath),
11090
- now: () => Date.now()
11385
+ now: () => Date.now(),
11386
+ loadedElsewhere: (onDisk) => processLoadedBuild({ entryPath: loaded.entryPath, ...onDisk })
11091
11387
  });
11092
11388
  }
11093
11389
  return watch;
@@ -12785,7 +13081,7 @@ import { EventEmitter as EventEmitter4 } from "events";
12785
13081
  import { randomUUID as randomUUID5 } from "crypto";
12786
13082
  import { existsSync as existsSync4, writeFileSync as writeFileSync9 } from "fs";
12787
13083
  import { unlink as unlink2 } from "fs/promises";
12788
- import { join as join16 } from "path";
13084
+ import { join as join17 } from "path";
12789
13085
 
12790
13086
  // src/claude-session-bun.ts
12791
13087
  import * as os6 from "os";
@@ -13967,7 +14263,7 @@ ${text2}`
13967
14263
  }
13968
14264
  function stageImage(data, extension) {
13969
14265
  try {
13970
- const file = join16(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
14266
+ const file = join17(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
13971
14267
  writeFileSync9(file, data, { mode: 384, flag: "wx" });
13972
14268
  return file;
13973
14269
  } catch (err) {
@@ -14451,15 +14747,15 @@ function spawnInteractiveProcess(opts) {
14451
14747
  // src/spawn-record-store.ts
14452
14748
  import { mkdirSync as mkdirSync9, readFileSync as readFileSync11, renameSync as renameSync7, statSync as statSync10, writeFileSync as writeFileSync10 } from "fs";
14453
14749
  import { homedir as homedir14 } from "os";
14454
- import { join as join17 } from "path";
14750
+ import { join as join18 } from "path";
14455
14751
  var MAX_SPAWN_RECORDS = 256;
14456
14752
  var SPAWN_RECORD_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
14457
14753
  var pathOverride6 = null;
14458
14754
  var lastWritten = /* @__PURE__ */ new Map();
14459
14755
  function spawnRecordStorePath() {
14460
14756
  if (pathOverride6) return pathOverride6;
14461
- const stateRoot = process.env.XDG_STATE_HOME || join17(homedir14(), ".local", "state");
14462
- return join17(stateRoot, "opencode-claude-code-plugin", "session-spawns.json");
14757
+ const stateRoot = process.env.XDG_STATE_HOME || join18(homedir14(), ".local", "state");
14758
+ return join18(stateRoot, "opencode-claude-code-plugin", "session-spawns.json");
14463
14759
  }
14464
14760
  function isRecord5(value) {
14465
14761
  const record = value;
@@ -14511,7 +14807,7 @@ ${record.agent ?? ""}`;
14511
14807
  const cutoff = now - SPAWN_RECORD_MAX_AGE_MS;
14512
14808
  const kept = [...merged.entries()].filter(([, entry]) => entry.at >= cutoff).sort((a, b) => a[1].at - b[1].at).slice(-MAX_SPAWN_RECORDS);
14513
14809
  try {
14514
- mkdirSync9(join17(file, ".."), { recursive: true, mode: 448 });
14810
+ mkdirSync9(join18(file, ".."), { recursive: true, mode: 448 });
14515
14811
  const tmp = `${file}.${process.pid}.tmp`;
14516
14812
  writeFileSync10(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
14517
14813
  mode: 384
@@ -14589,7 +14885,7 @@ function dedupe2(dirs) {
14589
14885
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
14590
14886
  import { homedir as homedir15 } from "os";
14591
14887
  import { randomUUID as randomUUID7 } from "crypto";
14592
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join18 } from "path";
14888
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join19 } from "path";
14593
14889
  function readPromptFileIfPresent(path15) {
14594
14890
  try {
14595
14891
  const content = readFileSync12(path15, "utf8").trim();
@@ -14601,7 +14897,7 @@ function readPromptFileIfPresent(path15) {
14601
14897
  function nearestWorkspaceAgentsPrompt(cwd) {
14602
14898
  let dir = cwd;
14603
14899
  while (true) {
14604
- const content = readPromptFileIfPresent(join18(dir, "AGENTS.md"));
14900
+ const content = readPromptFileIfPresent(join19(dir, "AGENTS.md"));
14605
14901
  if (content) return content;
14606
14902
  const parent = dirname6(dir);
14607
14903
  if (parent === dir) return void 0;
@@ -14753,8 +15049,8 @@ ${options.compressionSummary.trim()}`
14753
15049
  for (const s of extraSystemContent) {
14754
15050
  if (s.trim()) parts.push(s.trim());
14755
15051
  }
14756
- const configRoot = process.env.XDG_CONFIG_HOME ?? join18(homedir15(), ".config");
14757
- const globalAgents = readPromptFileIfPresent(join18(configRoot, "opencode", "AGENTS.md"));
15052
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join19(homedir15(), ".config");
15053
+ const globalAgents = readPromptFileIfPresent(join19(configRoot, "opencode", "AGENTS.md"));
14758
15054
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
14759
15055
  const forwarded = extraSystemContent.join("\n\n");
14760
15056
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -14770,7 +15066,7 @@ ${options.compressionSummary.trim()}`
14770
15066
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
14771
15067
  const content = parts.join("\n\n");
14772
15068
  if (!content) return void 0;
14773
- const path15 = join18(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
15069
+ const path15 = join19(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
14774
15070
  try {
14775
15071
  writeFileSync11(path15, content, { encoding: "utf8", mode: 384 });
14776
15072
  return path15;
@@ -18614,7 +18910,7 @@ var ClaudeCodeLanguageModel = class {
18614
18910
  let replayReason = "no-session-for-key";
18615
18911
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
18616
18912
  const configDir = resolveConfigDir(this.config.configDir);
18617
- const transcriptPath2 = (id, dir) => join19(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
18913
+ const transcriptPath2 = (id, dir) => join20(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
18618
18914
  const resumePoint = findResumePoint({
18619
18915
  sessionKey: sk,
18620
18916
  prompt: options.prompt,
@@ -20289,7 +20585,7 @@ import {
20289
20585
  writeFileSync as writeFileSync12
20290
20586
  } from "fs";
20291
20587
  import { homedir as homedir16 } from "os";
20292
- import { dirname as dirname7, join as join20, resolve as resolve6 } from "path";
20588
+ import { dirname as dirname7, join as join21, resolve as resolve6 } from "path";
20293
20589
  import { fileURLToPath as fileURLToPath4 } from "url";
20294
20590
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
20295
20591
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -20297,14 +20593,14 @@ var alreadyRan = false;
20297
20593
  function candidateCacheRoots() {
20298
20594
  const xdg = process.env.XDG_CACHE_HOME;
20299
20595
  return [
20300
- xdg ? join20(xdg, "opencode") : null,
20301
- join20(homedir16(), ".cache", "opencode"),
20302
- join20(homedir16(), "Library", "Caches", "opencode")
20596
+ xdg ? join21(xdg, "opencode") : null,
20597
+ join21(homedir16(), ".cache", "opencode"),
20598
+ join21(homedir16(), "Library", "Caches", "opencode")
20303
20599
  ].filter((p) => Boolean(p));
20304
20600
  }
20305
20601
  function userOpencodeJsonPath() {
20306
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join20(homedir16(), ".config");
20307
- return join20(xdgConfig, "opencode", "opencode.json");
20602
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join21(homedir16(), ".config");
20603
+ return join21(xdgConfig, "opencode", "opencode.json");
20308
20604
  }
20309
20605
  function userIntendsToUseUnscoped() {
20310
20606
  const cfg = userOpencodeJsonPath();
@@ -20331,15 +20627,15 @@ function ourLoadedDir() {
20331
20627
  function ourVersion(dir) {
20332
20628
  if (!dir) return null;
20333
20629
  try {
20334
- const pkg = JSON.parse(readFileSync14(join20(dir, "package.json"), "utf8"));
20630
+ const pkg = JSON.parse(readFileSync14(join21(dir, "package.json"), "utf8"));
20335
20631
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
20336
20632
  } catch {
20337
20633
  return null;
20338
20634
  }
20339
20635
  }
20340
20636
  function markerPath() {
20341
- const stateRoot = process.env.XDG_STATE_HOME ?? join20(homedir16(), ".local", "state");
20342
- return join20(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
20637
+ const stateRoot = process.env.XDG_STATE_HOME ?? join21(homedir16(), ".local", "state");
20638
+ return join21(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
20343
20639
  }
20344
20640
  function markerVersion() {
20345
20641
  try {
@@ -20388,7 +20684,7 @@ function cleanupStaleUnscopedInstall() {
20388
20684
  }
20389
20685
  function cleanupOne(cacheRoot, ourDir) {
20390
20686
  if (!existsSync5(cacheRoot)) return;
20391
- const stalePath = join20(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
20687
+ const stalePath = join21(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
20392
20688
  if (!existsSync5(stalePath)) return;
20393
20689
  let realStalePath = stalePath;
20394
20690
  try {
@@ -20396,7 +20692,7 @@ function cleanupOne(cacheRoot, ourDir) {
20396
20692
  } catch {
20397
20693
  }
20398
20694
  if (ourDir && realStalePath === ourDir) return;
20399
- const pkgJsonPath = join20(stalePath, "package.json");
20695
+ const pkgJsonPath = join21(stalePath, "package.json");
20400
20696
  if (!existsSync5(pkgJsonPath)) return;
20401
20697
  let pkg = {};
20402
20698
  try {
@@ -20416,7 +20712,7 @@ function cleanupOne(cacheRoot, ourDir) {
20416
20712
  });
20417
20713
  return;
20418
20714
  }
20419
- const cachePkgJson = join20(cacheRoot, "package.json");
20715
+ const cachePkgJson = join21(cacheRoot, "package.json");
20420
20716
  if (!existsSync5(cachePkgJson)) return;
20421
20717
  try {
20422
20718
  const cfg = JSON.parse(readFileSync14(cachePkgJson, "utf8"));
@@ -20822,22 +21118,125 @@ function stoppedSessionIdV2(event) {
20822
21118
  const id = event.data?.sessionID;
20823
21119
  return typeof id === "string" && id.length > 0 ? id : void 0;
20824
21120
  }
21121
+ function handleSessionEventHere(event) {
21122
+ noteSessionRunEvent(event);
21123
+ const deleted = deletedSessionIdV2(event);
21124
+ if (deleted) {
21125
+ const released = deleteActiveProcessesForSession(deleted);
21126
+ if (released.length > 0) {
21127
+ log.info("released claude state for deleted session", { sessionID: deleted, released });
21128
+ }
21129
+ return;
21130
+ }
21131
+ const stopped = stoppedSessionIdV2(event);
21132
+ if (stopped) releaseParkedProxyCallsForSession(stopped);
21133
+ }
21134
+ var ownCopy;
21135
+ var liveSetups = 0;
21136
+ var plannedByDirectory = /* @__PURE__ */ new Map();
21137
+ var forwardedProviders = /* @__PURE__ */ new Map();
21138
+ var MAX_FORWARDED_PROVIDERS = 32;
21139
+ function plannedSettings(directory, providerID) {
21140
+ const here = plannedByDirectory.get(directory)?.get(providerID);
21141
+ if (here) return here;
21142
+ for (const planned of plannedByDirectory.values()) {
21143
+ const found = planned.get(providerID);
21144
+ if (found) return found;
21145
+ }
21146
+ return void 0;
21147
+ }
21148
+ function pluginCopyFor(deps) {
21149
+ if (ownCopy) return ownCopy;
21150
+ const loaded = staleBuildWatch().loaded;
21151
+ ownCopy = {
21152
+ id: randomUUID8(),
21153
+ version: loaded.version,
21154
+ build: { entryPath: loaded.entryPath, mtimeMs: loaded.mtimeMs, size: loaded.size },
21155
+ disposed: false,
21156
+ holds: (sessionID) => sessionHeldHere(sessionID),
21157
+ quiet: () => noWorkInFlightHere(),
21158
+ release: (sessionID) => releaseIdleForHandover(sessionID),
21159
+ async modelFor(turn) {
21160
+ const planned = plannedSettings(turn.directory, turn.providerID);
21161
+ if (!planned) throw new Error(`no provider ${turn.providerID} is planned in this build`);
21162
+ const cacheKey = JSON.stringify([turn.directory ?? null, turn.providerID, turn.sdkOptions]);
21163
+ let provider = forwardedProviders.get(cacheKey);
21164
+ if (!provider) {
21165
+ const settings = await resolveSdkSettings(turn.providerID, planned, turn.sdkOptions);
21166
+ provider = deps.createProvider(settings);
21167
+ forwardedProviders.set(cacheKey, provider);
21168
+ while (forwardedProviders.size > MAX_FORWARDED_PROVIDERS) {
21169
+ const oldest = forwardedProviders.keys().next().value;
21170
+ if (oldest === void 0) break;
21171
+ forwardedProviders.delete(oldest);
21172
+ }
21173
+ }
21174
+ return provider.languageModel(turn.modelId);
21175
+ }
21176
+ };
21177
+ return ownCopy;
21178
+ }
21179
+ function retireWhenSpent() {
21180
+ const copy = ownCopy;
21181
+ if (!copy?.disposed || isActivePluginCopy(copy) || !holdsNoClaudeState()) return;
21182
+ unregisterPluginCopy(copy);
21183
+ leaveSessionEventRelay();
21184
+ log.info("a disposed plugin build left the process", { version: copy.version });
21185
+ }
21186
+ function routedModel(model, copy, turn) {
21187
+ const sessionOf = (options) => resolveSessionAffinity(
21188
+ options.headers,
21189
+ options.providerOptions,
21190
+ turn.providerID
21191
+ );
21192
+ return {
21193
+ specificationVersion: model.specificationVersion,
21194
+ provider: model.provider,
21195
+ modelId: model.modelId,
21196
+ supportedUrls: model.supportedUrls,
21197
+ doStream: (options) => routeTurn({
21198
+ own: copy,
21199
+ turn,
21200
+ sessionID: sessionOf(options),
21201
+ runHere: () => model.doStream(options),
21202
+ runOn: (other) => other.doStream(options)
21203
+ }),
21204
+ doGenerate: (options) => routeTurn({
21205
+ own: copy,
21206
+ turn,
21207
+ sessionID: sessionOf(options),
21208
+ runHere: () => model.doGenerate(options),
21209
+ runOn: (other) => other.doGenerate(options)
21210
+ })
21211
+ };
21212
+ }
21213
+ function routedProvider(provider, copy, base) {
21214
+ const languageModel = (modelId) => routedModel(provider.languageModel(modelId), copy, { ...base, modelId });
21215
+ return Object.assign((modelId) => languageModel(modelId), provider, {
21216
+ languageModel
21217
+ });
21218
+ }
21219
+ var closeOwnFeed;
21220
+ defineOwnRelayMember({
21221
+ handle: (event) => {
21222
+ handleSessionEventHere(event);
21223
+ retireWhenSpent();
21224
+ },
21225
+ feedOpened: () => {
21226
+ closeOwnFeed ??= openSessionRunEventFeed();
21227
+ },
21228
+ feedClosed: () => {
21229
+ const close = closeOwnFeed;
21230
+ closeOwnFeed = void 0;
21231
+ close?.();
21232
+ }
21233
+ });
20825
21234
  async function followSessionEvents(subscribe, signal) {
20826
- const closeFeed = openSessionRunEventFeed();
21235
+ const closeFollower = openRelayFollower();
20827
21236
  try {
20828
21237
  for await (const event of subscribe({ signal })) {
20829
21238
  if (signal.aborted) break;
20830
- noteSessionRunEvent(event);
20831
- const deleted = deletedSessionIdV2(event);
20832
- if (deleted) {
20833
- const released = deleteActiveProcessesForSession(deleted);
20834
- if (released.length > 0) {
20835
- log.info("released claude state for deleted session", { sessionID: deleted, released });
20836
- }
20837
- continue;
20838
- }
20839
- const stopped = stoppedSessionIdV2(event);
20840
- if (stopped) releaseParkedProxyCallsForSession(stopped);
21239
+ relaySessionEvent(event);
20841
21240
  }
20842
21241
  } catch (err) {
20843
21242
  if (!signal.aborted) {
@@ -20847,7 +21246,7 @@ async function followSessionEvents(subscribe, signal) {
20847
21246
  );
20848
21247
  }
20849
21248
  } finally {
20850
- closeFeed();
21249
+ closeFollower();
20851
21250
  }
20852
21251
  }
20853
21252
  var noticeEmitters = /* @__PURE__ */ new Set();
@@ -20935,6 +21334,11 @@ function createV2Setup(deps) {
20935
21334
  }
20936
21335
  }
20937
21336
  const planned = /* @__PURE__ */ new Map();
21337
+ const copy = pluginCopyFor(deps);
21338
+ copy.disposed = false;
21339
+ registerPluginCopy(copy);
21340
+ liveSetups += 1;
21341
+ plannedByDirectory.set(directory, planned);
20938
21342
  registrations.push(
20939
21343
  await ctx.provider.transform((editor) => {
20940
21344
  const seed = editor.get(BASE_PROVIDER_ID);
@@ -20978,7 +21382,11 @@ function createV2Setup(deps) {
20978
21382
  const providerID = event.model.providerID;
20979
21383
  if (!isClaudeCodeProviderId(providerID)) return;
20980
21384
  const settings = await resolveSdkSettings(providerID, planned.get(providerID), event.options);
20981
- event.sdk = deps.createProvider(settings);
21385
+ event.sdk = routedProvider(deps.createProvider(settings), copy, {
21386
+ directory,
21387
+ providerID,
21388
+ sdkOptions: event.options
21389
+ });
20982
21390
  log.debug("v2 sdk created", { providerID, cliPath: settings.cliPath });
20983
21391
  })
20984
21392
  );
@@ -21045,6 +21453,11 @@ function createV2Setup(deps) {
21045
21453
  });
21046
21454
  return async () => {
21047
21455
  events.abort();
21456
+ liveSetups = Math.max(0, liveSetups - 1);
21457
+ if (liveSetups === 0) {
21458
+ copy.disposed = true;
21459
+ if (!relayMemberHasServed()) unregisterPluginCopy(copy);
21460
+ }
21048
21461
  for (const registration of registrations.splice(0)) {
21049
21462
  await registration.dispose().catch(() => void 0);
21050
21463
  }