@khalilgharbaoui/opencode-claude-code-plugin 0.52.0 → 0.53.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 join13 } from "path";
2
+ import { join as join15 } from "path";
3
3
 
4
4
  // src/ids.ts
5
5
  import { randomInt } from "crypto";
@@ -1844,6 +1844,9 @@ function failoverUntil(resetsAt) {
1844
1844
  return resetsAtToMs(resetsAt);
1845
1845
  }
1846
1846
 
1847
+ // src/subagent-dispatch.ts
1848
+ import { join as join4 } from "path";
1849
+
1847
1850
  // src/agent-models.ts
1848
1851
  import { readFile, readdir } from "fs/promises";
1849
1852
  import path2 from "path";
@@ -2441,6 +2444,252 @@ function agentDirectories(home, projectDirectory) {
2441
2444
  return directories;
2442
2445
  }
2443
2446
 
2447
+ // src/dispatch-choice-store.ts
2448
+ import { mkdirSync as mkdirSync2, readFileSync, renameSync as renameSync2, writeFileSync } from "fs";
2449
+ import { homedir as homedir3 } from "os";
2450
+ import { join as join2 } from "path";
2451
+ var MAX_DISPATCH_CHOICE_SESSIONS = 128;
2452
+ var MAX_DISPATCH_CHOICES_PER_SESSION = 32;
2453
+ var DISPATCH_CHOICE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
2454
+ var sessions = null;
2455
+ var pathOverride = null;
2456
+ function dispatchChoiceStorePath() {
2457
+ if (pathOverride) return pathOverride;
2458
+ const stateRoot = process.env.XDG_STATE_HOME || join2(homedir3(), ".local", "state");
2459
+ return join2(stateRoot, "opencode-claude-code-plugin", "subagent-dispatch.json");
2460
+ }
2461
+ function isChoice(value) {
2462
+ const choice = value;
2463
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) return false;
2464
+ const ok = (field) => field === void 0 || typeof field === "string";
2465
+ if (!ok(choice.model) || !ok(choice.effort) || !ok(choice.account)) return false;
2466
+ return !!(choice.model || choice.effort || choice.account);
2467
+ }
2468
+ function isSessionRecord(value) {
2469
+ const record = value;
2470
+ if (!record || typeof record !== "object" || Array.isArray(record)) return false;
2471
+ if (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt)) {
2472
+ return false;
2473
+ }
2474
+ const choices = record.choices;
2475
+ if (!choices || typeof choices !== "object" || Array.isArray(choices)) return false;
2476
+ return Object.values(choices).every(isChoice);
2477
+ }
2478
+ function trimChoices(choices) {
2479
+ const entries = Object.entries(choices).filter(([, choice]) => isChoice(choice));
2480
+ return Object.fromEntries(entries.slice(-MAX_DISPATCH_CHOICES_PER_SESSION));
2481
+ }
2482
+ function readFile2(file) {
2483
+ const out = /* @__PURE__ */ new Map();
2484
+ try {
2485
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
2486
+ const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
2487
+ if (!entries || typeof entries !== "object") return out;
2488
+ const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
2489
+ const valid = Object.entries(entries).filter((entry) => isSessionRecord(entry[1])).filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
2490
+ for (const [key, record] of valid) out.set(key, record);
2491
+ } catch {
2492
+ }
2493
+ return out;
2494
+ }
2495
+ function load() {
2496
+ if (!sessions) sessions = readFile2(dispatchChoiceStorePath());
2497
+ return sessions;
2498
+ }
2499
+ function save(update) {
2500
+ const file = dispatchChoiceStorePath();
2501
+ const merged = readFile2(file);
2502
+ update(merged);
2503
+ const cutoff = Date.now() - DISPATCH_CHOICE_MAX_AGE_MS;
2504
+ const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).filter(([, record]) => Object.keys(record.choices).length > 0).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_DISPATCH_CHOICE_SESSIONS);
2505
+ sessions = new Map(kept);
2506
+ try {
2507
+ mkdirSync2(join2(file, ".."), { recursive: true, mode: 448 });
2508
+ const tmp = `${file}.${process.pid}.tmp`;
2509
+ writeFileSync(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
2510
+ mode: 384
2511
+ });
2512
+ renameSync2(tmp, file);
2513
+ } catch (err) {
2514
+ log.debug("could not write the subagent dispatch choice store", {
2515
+ error: err instanceof Error ? err.message : String(err)
2516
+ });
2517
+ }
2518
+ }
2519
+ function readDispatchChoice(opencodeSessionId, agent) {
2520
+ if (!opencodeSessionId || opencodeSessionId === "default") return void 0;
2521
+ const record = load().get(opencodeSessionId);
2522
+ const choice = record?.choices[agent];
2523
+ return choice ? { ...choice } : void 0;
2524
+ }
2525
+ function writeDispatchChoice(opencodeSessionId, agent, choice) {
2526
+ if (!opencodeSessionId || opencodeSessionId === "default" || !agent) return;
2527
+ const stored = {
2528
+ ...choice.model ? { model: choice.model } : {},
2529
+ ...choice.effort ? { effort: choice.effort } : {},
2530
+ ...choice.account ? { account: choice.account } : {}
2531
+ };
2532
+ if (!isChoice(stored)) return;
2533
+ const current = load().get(opencodeSessionId)?.choices[agent];
2534
+ if (current && current.model === stored.model && current.effort === stored.effort && current.account === stored.account) {
2535
+ return;
2536
+ }
2537
+ save((merged) => {
2538
+ const record = merged.get(opencodeSessionId) ?? { choices: {}, updatedAt: 0 };
2539
+ const choices = { ...record.choices };
2540
+ delete choices[agent];
2541
+ choices[agent] = stored;
2542
+ merged.delete(opencodeSessionId);
2543
+ merged.set(opencodeSessionId, {
2544
+ choices: trimChoices(choices),
2545
+ updatedAt: Date.now()
2546
+ });
2547
+ });
2548
+ }
2549
+ function forgetDispatchChoices(opencodeSessionId) {
2550
+ if (!load().has(opencodeSessionId)) return;
2551
+ save((merged) => {
2552
+ merged.delete(opencodeSessionId);
2553
+ });
2554
+ }
2555
+
2556
+ // src/tmp.ts
2557
+ import * as fs from "fs";
2558
+ import * as os from "os";
2559
+ import * as path3 from "path";
2560
+ var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
2561
+ var PID_TMP_DIR = path3.join(
2562
+ os.tmpdir(),
2563
+ `${PLUGIN_TMP_PREFIX}${process.pid}`
2564
+ );
2565
+ var currentDir = null;
2566
+ var createdDirs = /* @__PURE__ */ new Set();
2567
+ var registered = false;
2568
+ function currentUid() {
2569
+ return typeof process.getuid === "function" ? process.getuid() : void 0;
2570
+ }
2571
+ function untrustworthyReason(dir) {
2572
+ let stat;
2573
+ try {
2574
+ stat = fs.lstatSync(dir);
2575
+ } catch {
2576
+ return null;
2577
+ }
2578
+ if (stat.isSymbolicLink()) return "path is a symlink";
2579
+ if (!stat.isDirectory()) return "path is not a directory";
2580
+ const uid = currentUid();
2581
+ if (uid !== void 0 && stat.uid !== uid) {
2582
+ return `directory is owned by uid ${stat.uid}, not ${uid}`;
2583
+ }
2584
+ return null;
2585
+ }
2586
+ function ensureDir(dir) {
2587
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
2588
+ try {
2589
+ fs.chmodSync(dir, 448);
2590
+ } catch {
2591
+ }
2592
+ }
2593
+ function chooseDir() {
2594
+ const reason = untrustworthyReason(PID_TMP_DIR);
2595
+ if (!reason) return PID_TMP_DIR;
2596
+ const fallback = fs.mkdtempSync(path3.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
2597
+ log.warn("refusing the pid-named scratch directory", {
2598
+ path: PID_TMP_DIR,
2599
+ reason,
2600
+ fallback
2601
+ });
2602
+ return fallback;
2603
+ }
2604
+ function registerExitCleanup() {
2605
+ if (registered) return;
2606
+ registered = true;
2607
+ process.on("exit", () => {
2608
+ for (const dir of createdDirs) {
2609
+ try {
2610
+ fs.rmSync(dir, { recursive: true, force: true });
2611
+ } catch {
2612
+ }
2613
+ }
2614
+ });
2615
+ }
2616
+ var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
2617
+ function isProcessAlive(pid) {
2618
+ try {
2619
+ process.kill(pid, 0);
2620
+ return true;
2621
+ } catch (err) {
2622
+ return err.code === "EPERM";
2623
+ }
2624
+ }
2625
+ function sweepStalePluginTmpDirs(keep) {
2626
+ const removed = [];
2627
+ if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
2628
+ const root = os.tmpdir();
2629
+ let entries;
2630
+ try {
2631
+ entries = fs.readdirSync(root);
2632
+ } catch {
2633
+ return removed;
2634
+ }
2635
+ const uid = currentUid();
2636
+ const keepPath = keep ? path3.resolve(keep) : null;
2637
+ for (const entry of entries) {
2638
+ const match = PID_DIR_NAME.exec(entry);
2639
+ if (!match) continue;
2640
+ const pid = Number(match[1]);
2641
+ if (!Number.isSafeInteger(pid) || pid <= 0) continue;
2642
+ if (pid === process.pid) continue;
2643
+ const dir = path3.join(root, entry);
2644
+ if (keepPath && path3.resolve(dir) === keepPath) continue;
2645
+ let stat;
2646
+ try {
2647
+ stat = fs.lstatSync(dir);
2648
+ } catch {
2649
+ continue;
2650
+ }
2651
+ if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
2652
+ if (uid !== void 0 && stat.uid !== uid) continue;
2653
+ if (isProcessAlive(pid)) continue;
2654
+ try {
2655
+ fs.rmSync(dir, { recursive: true, force: true });
2656
+ removed.push(dir);
2657
+ } catch (err) {
2658
+ log.debug("could not remove a stale scratch directory", {
2659
+ dir,
2660
+ error: String(err)
2661
+ });
2662
+ }
2663
+ }
2664
+ if (removed.length > 0) {
2665
+ log.info("removed stale plugin scratch directories", {
2666
+ count: removed.length,
2667
+ dirs: removed
2668
+ });
2669
+ }
2670
+ return removed;
2671
+ }
2672
+ var swept = false;
2673
+ function sweepOnce(keep) {
2674
+ if (swept) return;
2675
+ swept = true;
2676
+ try {
2677
+ sweepStalePluginTmpDirs(keep);
2678
+ } catch (err) {
2679
+ log.warn("stale scratch directory sweep failed", { error: String(err) });
2680
+ }
2681
+ }
2682
+ function pluginTmpDir() {
2683
+ if (currentDir === null || untrustworthyReason(currentDir) !== null) {
2684
+ currentDir = chooseDir();
2685
+ }
2686
+ ensureDir(currentDir);
2687
+ createdDirs.add(currentDir);
2688
+ registerExitCleanup();
2689
+ sweepOnce(currentDir);
2690
+ return currentDir;
2691
+ }
2692
+
2444
2693
  // src/subagent-dispatch.ts
2445
2694
  var SUBAGENT_DISPATCH_MARKER = "\u258C **subagent dispatch:**";
2446
2695
  var SUBAGENT_DISPATCH_TOOL_CALL_PREFIX = "subagent_dispatch_";
@@ -2449,6 +2698,7 @@ var DEFAULT_ANSWER = "Default";
2449
2698
  var LAST_ANSWER_PREFIX = "Same as last time";
2450
2699
  var CUSTOMISE_ANSWER = "Customise\u2026";
2451
2700
  var PER_TASK_ANSWER = "Per task\u2026";
2701
+ var PER_TYPE_ANSWER = "Per type\u2026";
2452
2702
  var DISPATCH_COMBOS = [
2453
2703
  {
2454
2704
  model: "claude-haiku-5-5",
@@ -2487,24 +2737,37 @@ function isSubagentDispatchActive(input) {
2487
2737
  if (input.configured !== "ask") return false;
2488
2738
  return input.opencodeHasQuestion;
2489
2739
  }
2490
- var lastChoices = /* @__PURE__ */ new Map();
2491
- var MAX_REMEMBERED_CHOICES = 256;
2492
- function lastChoiceKey(parentSessionId, agent) {
2493
- return `${parentSessionId}${KEY_SEPARATOR3}${agent}`;
2494
- }
2495
2740
  function rememberDispatchChoice(parentSessionId, agent, choice) {
2496
- if (!parentSessionId || parentSessionId === "default") return;
2497
- const key = lastChoiceKey(parentSessionId, agent);
2498
- lastChoices.delete(key);
2499
- lastChoices.set(key, { ...choice });
2500
- while (lastChoices.size > MAX_REMEMBERED_CHOICES) {
2501
- const oldest = lastChoices.keys().next().value;
2502
- if (oldest === void 0) break;
2503
- lastChoices.delete(oldest);
2504
- }
2741
+ writeDispatchChoice(parentSessionId, agent, choice);
2505
2742
  }
2506
2743
  function recallDispatchChoice(parentSessionId, agent) {
2507
- return lastChoices.get(lastChoiceKey(parentSessionId, agent));
2744
+ return readDispatchChoice(parentSessionId, agent);
2745
+ }
2746
+ function forgetDispatchChoicesForSession(opencodeSessionId) {
2747
+ forgetDispatchChoices(opencodeSessionId);
2748
+ }
2749
+ function usableRemembered(context, agent) {
2750
+ const remembered = recallDispatchChoice(context.parentSessionId, agent);
2751
+ if (!remembered) return void 0;
2752
+ const choice = {};
2753
+ if (remembered.model && Object.hasOwn(defaultModels, remembered.model)) {
2754
+ choice.model = remembered.model;
2755
+ }
2756
+ if (remembered.effort && REASONING_EFFORTS.includes(remembered.effort)) {
2757
+ choice.effort = remembered.effort;
2758
+ }
2759
+ let droppedAccount;
2760
+ if (remembered.account) {
2761
+ if (dispatchAccountCandidates(context).includes(remembered.account)) {
2762
+ choice.account = remembered.account;
2763
+ } else {
2764
+ droppedAccount = remembered.account;
2765
+ }
2766
+ }
2767
+ if (!choice.model && !choice.effort && !choice.account) {
2768
+ return droppedAccount ? { choice, droppedAccount } : void 0;
2769
+ }
2770
+ return droppedAccount ? { choice, droppedAccount } : { choice };
2508
2771
  }
2509
2772
  var CLAIM_TTL_MS = 15 * 6e4;
2510
2773
  var MAX_CLAIMS = 512;
@@ -2636,7 +2899,11 @@ function comboLabel(model, effort) {
2636
2899
  return effort ? `${model} / ${effort}` : model;
2637
2900
  }
2638
2901
  function describeChoice(choice, fallback) {
2639
- return comboLabel(choice.model ?? fallback.model, choice.effort ?? fallback.effort);
2902
+ const combo = comboLabel(choice.model ?? fallback.model, choice.effort ?? fallback.effort);
2903
+ return choice.account ? `${combo} @${choice.account}` : combo;
2904
+ }
2905
+ function droppedAccountPhrase(account) {
2906
+ return ` The account "${account}" you picked last time is no longer available to this conversation, so this stays on the account you are on.`;
2640
2907
  }
2641
2908
  function availableCombos() {
2642
2909
  return DISPATCH_COMBOS.filter((combo) => Object.hasOwn(defaultModels, combo.model));
@@ -2654,16 +2921,25 @@ function dispatchAccountCandidates(context) {
2654
2921
  }
2655
2922
  return out;
2656
2923
  }
2657
- function accountQuestion(context) {
2924
+ function accountQuestion(context, scope) {
2658
2925
  const candidates = dispatchAccountCandidates(context);
2659
2926
  if (candidates.length < 2) return null;
2660
2927
  const source = candidates[0];
2928
+ const perType = "agent" in scope;
2929
+ const subject = perType ? `the "${scope.agent}" subagents` : "these subagents";
2930
+ const options = candidates.map((account) => ({
2931
+ label: account,
2932
+ description: account === source ? `Stay on this conversation's own account. This is what happens today.` : `Spawn ${subject} with "${account}"'s Claude config dir, so the work draws on that account's usage window.`
2933
+ }));
2934
+ if (!perType && scope.types > 1) {
2935
+ options.push({
2936
+ label: PER_TYPE_ANSWER,
2937
+ description: `Choose the account separately for each of these ${scope.types} agent types on the next screen.`
2938
+ });
2939
+ }
2661
2940
  return {
2662
- question: "Which Claude account should these subagents run on?",
2663
- options: candidates.map((account) => ({
2664
- label: account,
2665
- description: account === source ? `Stay on this conversation's own account. This is what happens today.` : `Spawn each subagent with "${account}"'s Claude config dir, so its work draws on that account's usage window.`
2666
- }))
2941
+ question: perType ? `Which Claude account should the "${scope.agent}" subagents run on?` : "Which Claude account should these subagents run on?",
2942
+ options
2667
2943
  };
2668
2944
  }
2669
2945
  function summaryQuestion(tasks, context) {
@@ -2674,13 +2950,14 @@ function summaryQuestion(tasks, context) {
2674
2950
  });
2675
2951
  const question = tasks.length === 1 ? `About to dispatch the "${tasks[0].agent}" subagent (${tasks[0].description || "no description"}). How should it run?` : `About to dispatch ${tasks.length} subagents (${counts.join(", ")}). How should they run?`;
2676
2952
  const options = [];
2677
- const remembered = types.map((agent) => recallDispatchChoice(context.parentSessionId, agent));
2678
- if (remembered.every((choice) => choice !== void 0)) {
2953
+ const remembered = types.map((agent) => usableRemembered(context, agent));
2954
+ if (remembered.every((entry) => entry !== void 0)) {
2955
+ const dropped = remembered.map((entry) => entry.droppedAccount).find((account) => !!account);
2679
2956
  options.push({
2680
2957
  label: LAST_ANSWER_PREFIX,
2681
2958
  description: types.map(
2682
- (agent, index) => `${agent}: ${describeChoice(remembered[index], context.describeDefault(agent))}`
2683
- ).join(". ")
2959
+ (agent, index) => `${agent}: ${describeChoice(remembered[index].choice, context.describeDefault(agent))}`
2960
+ ).join(". ") + (dropped ? `.${droppedAccountPhrase(dropped)}` : "")
2684
2961
  });
2685
2962
  }
2686
2963
  options.push({
@@ -2698,20 +2975,20 @@ function summaryQuestion(tasks, context) {
2698
2975
  }
2699
2976
  options.push({
2700
2977
  label: CUSTOMISE_ANSWER,
2701
- description: tasks.length === 1 ? "Choose the account, or type a model and effort of your own." : "Choose per agent type on the next screen, and per task after that."
2978
+ description: tasks.length === 1 ? "Choose the account, or type a model and effort of your own." : "Choose per agent type on the next screen, and per account or per task after that."
2702
2979
  });
2703
2980
  return { question, options };
2704
2981
  }
2705
2982
  function typeQuestion(agent, tasks, context) {
2706
2983
  const mine = tasks.filter((task) => task.agent === agent);
2707
2984
  const fallback = context.describeDefault(agent);
2708
- const question = mine.length === 1 ? `"${agent}" (${mine[0].description || "no description"}): how should it run?` : `"${agent}" (${mine.length} subagents): how should they run?`;
2985
+ const question = (mine.length === 1 ? `"${agent}" (${mine[0].description || "no description"}): how should it run?` : `"${agent}" (${mine.length} subagents): how should they run?`) + accountHint(context);
2709
2986
  const options = [];
2710
- const remembered = recallDispatchChoice(context.parentSessionId, agent);
2987
+ const remembered = usableRemembered(context, agent);
2711
2988
  if (remembered) {
2712
2989
  options.push({
2713
2990
  label: LAST_ANSWER_PREFIX,
2714
- description: `${describeChoice(remembered, fallback)}, the last thing you picked for "${agent}" in this conversation.`
2991
+ description: `${describeChoice(remembered.choice, fallback)}, the last thing you picked for "${agent}" in this conversation.` + (remembered.droppedAccount ? droppedAccountPhrase(remembered.droppedAccount) : "")
2715
2992
  });
2716
2993
  }
2717
2994
  options.push({
@@ -2735,7 +3012,7 @@ function typeQuestion(agent, tasks, context) {
2735
3012
  function taskQuestion(task, typeChoice, context) {
2736
3013
  const fallback = context.describeDefault(task.agent);
2737
3014
  const options = [];
2738
- if (typeChoice && (typeChoice.model || typeChoice.effort)) {
3015
+ if (typeChoice && (typeChoice.model || typeChoice.effort || typeChoice.account)) {
2739
3016
  options.push({
2740
3017
  label: `Same as the rest (${describeChoice(typeChoice, fallback)})`,
2741
3018
  description: `What you picked for every "${task.agent}" subagent in this dispatch.`
@@ -2749,10 +3026,15 @@ function taskQuestion(task, typeChoice, context) {
2749
3026
  options.push({ label: comboLabel(combo.model, combo.effort), description: combo.description });
2750
3027
  }
2751
3028
  return {
2752
- question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}`,
3029
+ question: `"${task.agent}": ${task.description || task.prompt.slice(0, 60)}${accountHint(context)}`,
2753
3030
  options
2754
3031
  };
2755
3032
  }
3033
+ function accountHint(context) {
3034
+ const candidates = dispatchAccountCandidates(context);
3035
+ if (candidates.length < 2) return "";
3036
+ return ` Add "@${candidates[1]}" to a typed answer to send it to that account.`;
3037
+ }
2756
3038
  function header(text) {
2757
3039
  return text.length <= 30 ? text : `${text.slice(0, 29)}\u2026`;
2758
3040
  }
@@ -2787,17 +3069,26 @@ function createSubagentDispatchQuestion(sessionKey2, heldCallIds, tasks, context
2787
3069
  fields: [{ kind: "all" }],
2788
3070
  choices: /* @__PURE__ */ new Map(),
2789
3071
  account: void 0,
3072
+ typeAccounts: /* @__PURE__ */ new Map(),
3073
+ typeChoices: /* @__PURE__ */ new Map(),
3074
+ droppedAccounts: [],
2790
3075
  context
2791
3076
  };
2792
3077
  const summary = summaryQuestion(tasks, context);
2793
3078
  return buildCall(entry, [{ header: "Subagents", ...summary }], toolCallId);
2794
3079
  }
2795
- function parseCustomChoice(answer) {
3080
+ function parseCustomChoice(answer, candidates = []) {
2796
3081
  const words2 = answer.trim().split(/[\s,/]+/).map((word) => word.trim()).filter(Boolean);
2797
3082
  if (words2.length === 0) return null;
2798
3083
  const choice = {};
2799
3084
  for (const word of words2) {
2800
3085
  const lower = word.toLowerCase();
3086
+ if (lower.startsWith("@")) {
3087
+ const account = normalizeAccountName(lower.slice(1));
3088
+ if (!account || !candidates.includes(account)) return null;
3089
+ choice.account = account;
3090
+ continue;
3091
+ }
2801
3092
  if (REASONING_EFFORTS.includes(lower)) {
2802
3093
  choice.effort = lower;
2803
3094
  continue;
@@ -2808,7 +3099,7 @@ function parseCustomChoice(answer) {
2808
3099
  }
2809
3100
  return null;
2810
3101
  }
2811
- return choice.model || choice.effort ? choice : null;
3102
+ return choice.model || choice.effort || choice.account ? choice : null;
2812
3103
  }
2813
3104
  function parseComboLabel(answer) {
2814
3105
  for (const combo of availableCombos()) {
@@ -2818,7 +3109,7 @@ function parseComboLabel(answer) {
2818
3109
  }
2819
3110
  return null;
2820
3111
  }
2821
- function classifyAnswer(answer) {
3112
+ function classifyAnswer(answer, candidates = []) {
2822
3113
  const trimmed = answer.trim();
2823
3114
  if (!trimmed) return { kind: "default" };
2824
3115
  if (trimmed === DEFAULT_ANSWER) return { kind: "default" };
@@ -2828,7 +3119,7 @@ function classifyAnswer(answer) {
2828
3119
  if (trimmed.startsWith("Same as the rest")) return { kind: "sameAsRest" };
2829
3120
  const combo = parseComboLabel(trimmed);
2830
3121
  if (combo) return { kind: "choice", choice: combo };
2831
- const custom = parseCustomChoice(trimmed);
3122
+ const custom = parseCustomChoice(trimmed, candidates);
2832
3123
  if (custom) return { kind: "choice", choice: custom };
2833
3124
  return { kind: "unknown", answer: trimmed };
2834
3125
  }
@@ -2869,19 +3160,28 @@ function advance(entry, part) {
2869
3160
  case "types":
2870
3161
  return advanceTypes(entry, answers);
2871
3162
  default:
2872
- return advanceTasks(entry, answers);
3163
+ return advanceDetail(entry, answers);
2873
3164
  }
2874
3165
  }
2875
3166
  function applyToAll(entry, choice) {
2876
3167
  for (const index of entry.tasks.keys()) entry.choices.set(index, { ...choice });
2877
3168
  }
3169
+ function noteDroppedAccount(entry, agent, account) {
3170
+ if (entry.droppedAccounts.some((dropped) => dropped.agent === agent)) return;
3171
+ entry.droppedAccounts.push({ agent, account });
3172
+ }
2878
3173
  function advanceSummary(entry, answers) {
2879
- const answer = classifyAnswer(answers.get(entry.questions[0]) ?? "");
3174
+ const candidates = dispatchAccountCandidates(entry.context);
3175
+ const answer = classifyAnswer(answers.get(entry.questions[0]) ?? "", candidates);
2880
3176
  if (answer.kind === "customise") return typesForm(entry);
2881
3177
  if (answer.kind === "last") {
2882
3178
  for (const [index, task] of entry.tasks.entries()) {
2883
- const remembered = recallDispatchChoice(entry.context.parentSessionId, task.agent);
2884
- if (remembered) entry.choices.set(index, { ...remembered });
3179
+ const remembered = usableRemembered(entry.context, task.agent);
3180
+ if (!remembered) continue;
3181
+ if (remembered.droppedAccount) {
3182
+ noteDroppedAccount(entry, task.agent, remembered.droppedAccount);
3183
+ }
3184
+ entry.choices.set(index, { ...remembered.choice });
2885
3185
  }
2886
3186
  return release(entry, null);
2887
3187
  }
@@ -2908,7 +3208,7 @@ function typesForm(entry) {
2908
3208
  ...typeQuestion(agent, entry.tasks, entry.context)
2909
3209
  }));
2910
3210
  const fields = types.map((agent) => ({ kind: "type", agent }));
2911
- const account = accountQuestion(entry.context);
3211
+ const account = accountQuestion(entry.context, { types: types.length });
2912
3212
  if (account) {
2913
3213
  prompts.push({ header: "Account", ...account });
2914
3214
  fields.push({ kind: "account" });
@@ -2920,25 +3220,30 @@ function typesForm(entry) {
2920
3220
  call: buildCall(entry, prompts, questionCallId("types"))
2921
3221
  };
2922
3222
  }
3223
+ function readAccountAnswer(entry, raw, agent) {
3224
+ const trimmed = raw.trim();
3225
+ if (!trimmed || trimmed === DEFAULT_ANSWER) return void 0;
3226
+ const picked = normalizeAccountName(trimmed.startsWith("@") ? trimmed.slice(1) : trimmed);
3227
+ const candidates = dispatchAccountCandidates(entry.context);
3228
+ if (picked && candidates.includes(picked)) return picked;
3229
+ log.notice("subagent dispatch account answer refused", {
3230
+ sessionKey: entry.sessionKey,
3231
+ agent,
3232
+ answer: trimmed,
3233
+ candidates
3234
+ });
3235
+ return void 0;
3236
+ }
2923
3237
  function advanceTypes(entry, answers) {
3238
+ const candidates = dispatchAccountCandidates(entry.context);
2924
3239
  const perTask = [];
2925
- const typeChoices = /* @__PURE__ */ new Map();
3240
+ let perTypeAccounts = false;
2926
3241
  for (const [position, field] of entry.fields.entries()) {
2927
3242
  const raw = answers.get(entry.questions[position]) ?? "";
2928
- const answer = classifyAnswer(raw);
3243
+ const answer = classifyAnswer(raw, candidates);
2929
3244
  if (field.kind === "account") {
2930
- if (answer.kind === "unknown") {
2931
- const picked = normalizeAccountName(answer.answer);
2932
- const candidates = dispatchAccountCandidates(entry.context);
2933
- if (candidates.includes(picked)) entry.account = picked;
2934
- else {
2935
- log.notice("subagent dispatch account answer refused", {
2936
- sessionKey: entry.sessionKey,
2937
- answer: answer.answer,
2938
- candidates
2939
- });
2940
- }
2941
- }
3245
+ if (raw.trim() === PER_TYPE_ANSWER) perTypeAccounts = true;
3246
+ else entry.account = readAccountAnswer(entry, raw, null) ?? entry.account;
2942
3247
  continue;
2943
3248
  }
2944
3249
  if (field.kind !== "type") continue;
@@ -2947,12 +3252,15 @@ function advanceTypes(entry, answers) {
2947
3252
  continue;
2948
3253
  }
2949
3254
  if (answer.kind === "last") {
2950
- const remembered = recallDispatchChoice(entry.context.parentSessionId, field.agent);
2951
- if (remembered) typeChoices.set(field.agent, { ...remembered });
3255
+ const remembered = usableRemembered(entry.context, field.agent);
3256
+ if (remembered?.droppedAccount) {
3257
+ noteDroppedAccount(entry, field.agent, remembered.droppedAccount);
3258
+ }
3259
+ if (remembered) applyTypeChoice(entry, field.agent, remembered.choice);
2952
3260
  continue;
2953
3261
  }
2954
3262
  if (answer.kind === "choice") {
2955
- typeChoices.set(field.agent, answer.choice);
3263
+ applyTypeChoice(entry, field.agent, answer.choice);
2956
3264
  continue;
2957
3265
  }
2958
3266
  if (answer.kind === "unknown") {
@@ -2964,31 +3272,60 @@ function advanceTypes(entry, answers) {
2964
3272
  }
2965
3273
  }
2966
3274
  for (const [index, task] of entry.tasks.entries()) {
2967
- const choice = typeChoices.get(task.agent);
3275
+ const choice = entry.typeChoices.get(task.agent);
2968
3276
  if (choice) entry.choices.set(index, { ...choice });
2969
3277
  }
2970
- if (perTask.length > 0) return tasksForm(entry, perTask, typeChoices);
3278
+ if (perTask.length > 0 || perTypeAccounts) {
3279
+ return detailForm(entry, perTask, perTypeAccounts);
3280
+ }
2971
3281
  return release(entry, null);
2972
3282
  }
2973
- function tasksForm(entry, agents, typeChoices) {
3283
+ function applyTypeChoice(entry, agent, choice) {
3284
+ const { account, ...rest } = choice;
3285
+ if (account) entry.typeAccounts.set(agent, account);
3286
+ entry.typeChoices.set(agent, rest);
3287
+ }
3288
+ function detailForm(entry, perTaskAgents, perTypeAccounts) {
2974
3289
  const prompts = [];
2975
3290
  const fields = [];
3291
+ if (perTypeAccounts) {
3292
+ for (const agent of dispatchAgentTypes(entry.tasks)) {
3293
+ const account = accountQuestion(entry.context, { agent });
3294
+ if (!account) continue;
3295
+ prompts.push({ header: `${agent} account`, ...account });
3296
+ fields.push({ kind: "typeAccount", agent });
3297
+ }
3298
+ }
2976
3299
  for (const [index, task] of entry.tasks.entries()) {
2977
- if (!agents.includes(task.agent)) continue;
3300
+ if (!perTaskAgents.includes(task.agent)) continue;
3301
+ const typeChoice = entry.typeChoices.get(task.agent);
3302
+ const typeAccount = entry.typeAccounts.get(task.agent);
2978
3303
  prompts.push({
2979
3304
  header: task.description || task.agent,
2980
- ...taskQuestion(task, typeChoices.get(task.agent), entry.context)
3305
+ ...taskQuestion(
3306
+ task,
3307
+ typeChoice || typeAccount ? { ...typeChoice, ...typeAccount ? { account: typeAccount } : {} } : void 0,
3308
+ entry.context
3309
+ )
2981
3310
  });
2982
3311
  fields.push({ kind: "task", taskIndex: index });
2983
3312
  }
2984
- entry.stage = "tasks";
3313
+ if (prompts.length === 0) return release(entry, null);
3314
+ entry.stage = "detail";
2985
3315
  entry.fields = fields;
2986
- return { kind: "form", call: buildCall(entry, prompts, questionCallId("tasks")) };
3316
+ return { kind: "form", call: buildCall(entry, prompts, questionCallId("detail")) };
2987
3317
  }
2988
- function advanceTasks(entry, answers) {
3318
+ function advanceDetail(entry, answers) {
3319
+ const candidates = dispatchAccountCandidates(entry.context);
2989
3320
  for (const [position, field] of entry.fields.entries()) {
3321
+ const raw = answers.get(entry.questions[position]) ?? "";
3322
+ if (field.kind === "typeAccount") {
3323
+ const picked = readAccountAnswer(entry, raw, field.agent);
3324
+ if (picked) entry.typeAccounts.set(field.agent, picked);
3325
+ continue;
3326
+ }
2990
3327
  if (field.kind !== "task") continue;
2991
- const answer = classifyAnswer(answers.get(entry.questions[position]) ?? "");
3328
+ const answer = classifyAnswer(raw, candidates);
2992
3329
  if (answer.kind === "choice") {
2993
3330
  entry.choices.set(field.taskIndex, answer.choice);
2994
3331
  continue;
@@ -3008,13 +3345,18 @@ function advanceTasks(entry, answers) {
3008
3345
  }
3009
3346
  return release(entry, null);
3010
3347
  }
3348
+ function droppedAccountNote(dropped) {
3349
+ const list2 = dropped.map((entry) => `"${entry.agent}" (was "${entry.account}")`).join(", ");
3350
+ return `
3351
+ ${SUBAGENT_DISPATCH_MARKER} The account you last picked for ${list2} is no longer configured for this conversation, or no longer in its account group, so that work stays on the account you are on. Everything else you picked still applies.
3352
+ `;
3353
+ }
3011
3354
  function release(entry, note) {
3012
- if (entry.account) {
3013
- for (const [index, task] of entry.tasks.entries()) {
3014
- const current = entry.choices.get(index) ?? {};
3015
- entry.choices.set(index, { ...current, account: entry.account });
3016
- void task;
3017
- }
3355
+ for (const [index, task] of entry.tasks.entries()) {
3356
+ const current = entry.choices.get(index);
3357
+ const account = current?.account ?? entry.typeAccounts.get(task.agent) ?? entry.account;
3358
+ if (!account) continue;
3359
+ entry.choices.set(index, { ...current ?? {}, account });
3018
3360
  }
3019
3361
  const seen = /* @__PURE__ */ new Set();
3020
3362
  for (const [index, task] of entry.tasks.entries()) {
@@ -3024,6 +3366,15 @@ function release(entry, note) {
3024
3366
  seen.add(task.agent);
3025
3367
  rememberDispatchChoice(entry.context.parentSessionId, task.agent, choice);
3026
3368
  }
3369
+ if (!note && entry.droppedAccounts.length > 0) {
3370
+ log.notice("a remembered subagent dispatch account is no longer available", {
3371
+ sessionKey: entry.sessionKey,
3372
+ // `agent` rather than a key of its own: `dropped` already means a count
3373
+ // in the bundle allowlist, and these are agent names like every other.
3374
+ agent: entry.droppedAccounts.map((dropped) => dropped.agent)
3375
+ });
3376
+ note = droppedAccountNote(entry.droppedAccounts);
3377
+ }
3027
3378
  return {
3028
3379
  kind: "release",
3029
3380
  heldCallIds: entry.heldCallIds,
@@ -3060,8 +3411,8 @@ import { execFile } from "child_process";
3060
3411
  import { promisify } from "util";
3061
3412
 
3062
3413
  // src/windows-spawn.ts
3063
- import * as fs from "fs";
3064
- import * as path3 from "path";
3414
+ import * as fs2 from "fs";
3415
+ import * as path4 from "path";
3065
3416
  var DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
3066
3417
  function envValue(env, name) {
3067
3418
  const direct = env[name];
@@ -3071,7 +3422,7 @@ function envValue(env, name) {
3071
3422
  }
3072
3423
  function defaultIsFile(candidate) {
3073
3424
  try {
3074
- return fs.statSync(candidate).isFile();
3425
+ return fs2.statSync(candidate).isFile();
3075
3426
  } catch {
3076
3427
  return false;
3077
3428
  }
@@ -3081,7 +3432,7 @@ function executableExtensions(env = process.env) {
3081
3432
  return raw.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean).map((ext) => ext.startsWith(".") ? ext : `.${ext}`);
3082
3433
  }
3083
3434
  function isBatchFile(file) {
3084
- const ext = path3.win32.extname(file).toLowerCase();
3435
+ const ext = path4.win32.extname(file).toLowerCase();
3085
3436
  return ext === ".cmd" || ext === ".bat";
3086
3437
  }
3087
3438
  function resolveWindowsCommand(command, deps = {}) {
@@ -3089,7 +3440,7 @@ function resolveWindowsCommand(command, deps = {}) {
3089
3440
  const isFile = deps.isFile ?? defaultIsFile;
3090
3441
  const extensions = executableExtensions(env);
3091
3442
  const candidatesFor = (base) => {
3092
- const ext = path3.win32.extname(base).toLowerCase();
3443
+ const ext = path4.win32.extname(base).toLowerCase();
3093
3444
  if (ext && extensions.includes(ext)) return [base];
3094
3445
  return [...extensions.map((candidate) => base + candidate), base];
3095
3446
  };
@@ -3100,10 +3451,10 @@ function resolveWindowsCommand(command, deps = {}) {
3100
3451
  return null;
3101
3452
  }
3102
3453
  const pathValue = envValue(env, "PATH") ?? "";
3103
- for (const entry of pathValue.split(path3.win32.delimiter)) {
3454
+ for (const entry of pathValue.split(path4.win32.delimiter)) {
3104
3455
  const directory = entry.trim().replace(/^"(.*)"$/, "$1");
3105
3456
  if (!directory) continue;
3106
- for (const candidate of candidatesFor(path3.win32.join(directory, command))) {
3457
+ for (const candidate of candidatesFor(path4.win32.join(directory, command))) {
3107
3458
  if (isFile(candidate)) return candidate;
3108
3459
  }
3109
3460
  }
@@ -3132,7 +3483,7 @@ function quoteBatchArgument(arg) {
3132
3483
  }
3133
3484
  function planCmdInvocation(file, args, env = process.env) {
3134
3485
  const comspec = envValue(env, "ComSpec") ?? "cmd.exe";
3135
- const line = [`"${path3.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
3486
+ const line = [`"${path4.win32.normalize(file)}"`, ...args.map(quoteBatchArgument)];
3136
3487
  return {
3137
3488
  file: comspec,
3138
3489
  args: ["/d", "/s", "/c", `"${line.join(" ")}"`],
@@ -3376,145 +3727,6 @@ import * as fs3 from "fs";
3376
3727
  import * as path5 from "path";
3377
3728
  import * as crypto from "crypto";
3378
3729
  import { EventEmitter } from "events";
3379
-
3380
- // src/tmp.ts
3381
- import * as fs2 from "fs";
3382
- import * as os from "os";
3383
- import * as path4 from "path";
3384
- var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
3385
- var PID_TMP_DIR = path4.join(
3386
- os.tmpdir(),
3387
- `${PLUGIN_TMP_PREFIX}${process.pid}`
3388
- );
3389
- var currentDir = null;
3390
- var createdDirs = /* @__PURE__ */ new Set();
3391
- var registered = false;
3392
- function currentUid() {
3393
- return typeof process.getuid === "function" ? process.getuid() : void 0;
3394
- }
3395
- function untrustworthyReason(dir) {
3396
- let stat;
3397
- try {
3398
- stat = fs2.lstatSync(dir);
3399
- } catch {
3400
- return null;
3401
- }
3402
- if (stat.isSymbolicLink()) return "path is a symlink";
3403
- if (!stat.isDirectory()) return "path is not a directory";
3404
- const uid = currentUid();
3405
- if (uid !== void 0 && stat.uid !== uid) {
3406
- return `directory is owned by uid ${stat.uid}, not ${uid}`;
3407
- }
3408
- return null;
3409
- }
3410
- function ensureDir(dir) {
3411
- fs2.mkdirSync(dir, { recursive: true, mode: 448 });
3412
- try {
3413
- fs2.chmodSync(dir, 448);
3414
- } catch {
3415
- }
3416
- }
3417
- function chooseDir() {
3418
- const reason = untrustworthyReason(PID_TMP_DIR);
3419
- if (!reason) return PID_TMP_DIR;
3420
- const fallback = fs2.mkdtempSync(path4.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
3421
- log.warn("refusing the pid-named scratch directory", {
3422
- path: PID_TMP_DIR,
3423
- reason,
3424
- fallback
3425
- });
3426
- return fallback;
3427
- }
3428
- function registerExitCleanup() {
3429
- if (registered) return;
3430
- registered = true;
3431
- process.on("exit", () => {
3432
- for (const dir of createdDirs) {
3433
- try {
3434
- fs2.rmSync(dir, { recursive: true, force: true });
3435
- } catch {
3436
- }
3437
- }
3438
- });
3439
- }
3440
- var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
3441
- function isProcessAlive(pid) {
3442
- try {
3443
- process.kill(pid, 0);
3444
- return true;
3445
- } catch (err) {
3446
- return err.code === "EPERM";
3447
- }
3448
- }
3449
- function sweepStalePluginTmpDirs(keep) {
3450
- const removed = [];
3451
- if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
3452
- const root = os.tmpdir();
3453
- let entries;
3454
- try {
3455
- entries = fs2.readdirSync(root);
3456
- } catch {
3457
- return removed;
3458
- }
3459
- const uid = currentUid();
3460
- const keepPath = keep ? path4.resolve(keep) : null;
3461
- for (const entry of entries) {
3462
- const match = PID_DIR_NAME.exec(entry);
3463
- if (!match) continue;
3464
- const pid = Number(match[1]);
3465
- if (!Number.isSafeInteger(pid) || pid <= 0) continue;
3466
- if (pid === process.pid) continue;
3467
- const dir = path4.join(root, entry);
3468
- if (keepPath && path4.resolve(dir) === keepPath) continue;
3469
- let stat;
3470
- try {
3471
- stat = fs2.lstatSync(dir);
3472
- } catch {
3473
- continue;
3474
- }
3475
- if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
3476
- if (uid !== void 0 && stat.uid !== uid) continue;
3477
- if (isProcessAlive(pid)) continue;
3478
- try {
3479
- fs2.rmSync(dir, { recursive: true, force: true });
3480
- removed.push(dir);
3481
- } catch (err) {
3482
- log.debug("could not remove a stale scratch directory", {
3483
- dir,
3484
- error: String(err)
3485
- });
3486
- }
3487
- }
3488
- if (removed.length > 0) {
3489
- log.info("removed stale plugin scratch directories", {
3490
- count: removed.length,
3491
- dirs: removed
3492
- });
3493
- }
3494
- return removed;
3495
- }
3496
- var swept = false;
3497
- function sweepOnce(keep) {
3498
- if (swept) return;
3499
- swept = true;
3500
- try {
3501
- sweepStalePluginTmpDirs(keep);
3502
- } catch (err) {
3503
- log.warn("stale scratch directory sweep failed", { error: String(err) });
3504
- }
3505
- }
3506
- function pluginTmpDir() {
3507
- if (currentDir === null || untrustworthyReason(currentDir) !== null) {
3508
- currentDir = chooseDir();
3509
- }
3510
- ensureDir(currentDir);
3511
- createdDirs.add(currentDir);
3512
- registerExitCleanup();
3513
- sweepOnce(currentDir);
3514
- return currentDir;
3515
- }
3516
-
3517
- // src/proxy-mcp.ts
3518
3730
  var PROXY_KEEPALIVE_MS = 15e3;
3519
3731
  function acceptsEventStream(acceptHeader) {
3520
3732
  return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
@@ -6023,17 +6235,17 @@ function tailIsReplyOnly(history, from) {
6023
6235
  }
6024
6236
 
6025
6237
  // src/session-resume-store.ts
6026
- import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
6027
- import { homedir as homedir5 } from "os";
6028
- import { join as join6 } from "path";
6238
+ import { existsSync as existsSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
6239
+ import { homedir as homedir6 } from "os";
6240
+ import { join as join8 } from "path";
6029
6241
  var MAX_RESUME_RECORDS = 256;
6030
6242
  var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
6031
6243
  var records = null;
6032
- var pathOverride = null;
6244
+ var pathOverride2 = null;
6033
6245
  function resumeStorePath() {
6034
- if (pathOverride) return pathOverride;
6035
- const stateRoot = process.env.XDG_STATE_HOME || join6(homedir5(), ".local", "state");
6036
- return join6(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
6246
+ if (pathOverride2) return pathOverride2;
6247
+ const stateRoot = process.env.XDG_STATE_HOME || join8(homedir6(), ".local", "state");
6248
+ return join8(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
6037
6249
  }
6038
6250
  function isDigest(value) {
6039
6251
  const entry = value;
@@ -6043,11 +6255,11 @@ function isRecord2(value) {
6043
6255
  const record = value;
6044
6256
  return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest)) && (record.configDir === void 0 || typeof record.configDir === "string") && (record.account === void 0 || typeof record.account === "string");
6045
6257
  }
6046
- function load() {
6258
+ function load2() {
6047
6259
  if (records) return records;
6048
6260
  records = /* @__PURE__ */ new Map();
6049
6261
  try {
6050
- const parsed = JSON.parse(readFileSync3(resumeStorePath(), "utf8"));
6262
+ const parsed = JSON.parse(readFileSync4(resumeStorePath(), "utf8"));
6051
6263
  const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
6052
6264
  if (entries && typeof entries === "object") {
6053
6265
  const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
@@ -6058,11 +6270,11 @@ function load() {
6058
6270
  }
6059
6271
  return records;
6060
6272
  }
6061
- function save(update) {
6273
+ function save2(update) {
6062
6274
  const file = resumeStorePath();
6063
6275
  const merged = /* @__PURE__ */ new Map();
6064
6276
  try {
6065
- const onDisk = JSON.parse(readFileSync3(file, "utf8"))?.sessions;
6277
+ const onDisk = JSON.parse(readFileSync4(file, "utf8"))?.sessions;
6066
6278
  if (onDisk && typeof onDisk === "object") {
6067
6279
  for (const [key, record] of Object.entries(onDisk)) {
6068
6280
  if (isRecord2(record)) merged.set(key, record);
@@ -6075,12 +6287,12 @@ function save(update) {
6075
6287
  const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
6076
6288
  records = new Map(kept);
6077
6289
  try {
6078
- mkdirSync3(join6(file, ".."), { recursive: true, mode: 448 });
6290
+ mkdirSync4(join8(file, ".."), { recursive: true, mode: 448 });
6079
6291
  const tmp = `${file}.${process.pid}.tmp`;
6080
- writeFileSync3(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
6292
+ writeFileSync4(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
6081
6293
  mode: 384
6082
6294
  });
6083
- renameSync2(tmp, file);
6295
+ renameSync3(tmp, file);
6084
6296
  } catch (err) {
6085
6297
  log.debug("could not write the claude session resume store", {
6086
6298
  error: err instanceof Error ? err.message : String(err)
@@ -6093,11 +6305,11 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
6093
6305
  const shape2 = conversationDigests(prompt, { assistantContent: false });
6094
6306
  const configDir = where?.configDir;
6095
6307
  const account = where?.account;
6096
- const current = load().get(sessionKey2);
6308
+ const current = load2().get(sessionKey2);
6097
6309
  if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.configDir === configDir && current.account === account && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
6098
6310
  return;
6099
6311
  }
6100
- save((merged) => {
6312
+ save2((merged) => {
6101
6313
  merged.delete(sessionKey2);
6102
6314
  merged.set(sessionKey2, {
6103
6315
  claudeSessionId,
@@ -6111,22 +6323,22 @@ function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath, where)
6111
6323
  });
6112
6324
  }
6113
6325
  function forgetResumePoint(sessionKey2) {
6114
- if (!load().has(sessionKey2)) return;
6115
- save((merged) => {
6326
+ if (!load2().has(sessionKey2)) return;
6327
+ save2((merged) => {
6116
6328
  merged.delete(sessionKey2);
6117
6329
  });
6118
6330
  }
6119
6331
  function moveResumePoint(fromKey, toKey) {
6120
- const record = load().get(fromKey);
6332
+ const record = load2().get(fromKey);
6121
6333
  if (!record) return;
6122
- save((merged) => {
6334
+ save2((merged) => {
6123
6335
  merged.delete(fromKey);
6124
6336
  if (merged.has(toKey)) return;
6125
6337
  merged.set(toKey, { ...record, updatedAt: Date.now() });
6126
6338
  });
6127
6339
  }
6128
6340
  function findResumePoint(opts) {
6129
- const record = load().get(opts.sessionKey);
6341
+ const record = load2().get(opts.sessionKey);
6130
6342
  if (!record) {
6131
6343
  opts.onRefused?.("no-session-recorded");
6132
6344
  return void 0;
@@ -6159,7 +6371,7 @@ function findSiblingResumePoint(opts) {
6159
6371
  let best;
6160
6372
  let bestUpdatedAt = -1;
6161
6373
  let refusal;
6162
- for (const [key, record] of load()) {
6374
+ for (const [key, record] of load2()) {
6163
6375
  if (key === opts.sessionKey) continue;
6164
6376
  if (modelSiblingSignature(key, providers) !== signature) continue;
6165
6377
  const siblingAccount = providers ? sessionKeyAccount(key, providers) : void 0;
@@ -6228,7 +6440,7 @@ function findForeignAccountSibling(opts) {
6228
6440
  }
6229
6441
  let found;
6230
6442
  let foundUpdatedAt = -1;
6231
- for (const [key, record] of load()) {
6443
+ for (const [key, record] of load2()) {
6232
6444
  const hit = consider(key);
6233
6445
  if (hit && record.updatedAt >= foundUpdatedAt) {
6234
6446
  found = hit;
@@ -6848,6 +7060,7 @@ function deleteActiveProcessesForSession(sessionID) {
6848
7060
  }
6849
7061
  for (const key of released) clearBackgroundTasks(key);
6850
7062
  forgetSessionChoice(sessionID);
7063
+ forgetDispatchChoicesForSession(sessionID);
6851
7064
  return released;
6852
7065
  }
6853
7066
  function killAllActiveProcesses() {
@@ -7583,7 +7796,7 @@ async function handleBtwCommand(client, input, options = {}) {
7583
7796
  // src/diagnostic-bundle.ts
7584
7797
  import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
7585
7798
  import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
7586
- import { homedir as homedir6 } from "os";
7799
+ import { homedir as homedir7 } from "os";
7587
7800
 
7588
7801
  // src/log-messages.ts
7589
7802
  var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
@@ -7593,6 +7806,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
7593
7806
  "Code Mode execute not forwarded: read-only permission preset",
7594
7807
  "MCP server asked for operator input and was declined: a headless Claude Code session cannot prompt, so its elicitation can only be refused. Run that server's flow in Claude Code directly, or configure it not to elicit.",
7595
7808
  "MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
7809
+ "a remembered subagent dispatch account is no longer available",
7596
7810
  "account failover declined; ending the turn",
7597
7811
  "accountGroups must be an object mapping an account name to a group name; ignoring it",
7598
7812
  "accountGroups names an account that is not configured; ignoring it",
@@ -7974,7 +8188,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
7974
8188
  return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
7975
8189
  }
7976
8190
  function createRedactionContext(options = {}) {
7977
- const home = options.home ?? homedir6();
8191
+ const home = options.home ?? homedir7();
7978
8192
  const homes = /* @__PURE__ */ new Set();
7979
8193
  if (home) homes.add(home);
7980
8194
  if (home) {
@@ -10116,9 +10330,9 @@ async function resolveSkillPluginDirs(opts) {
10116
10330
  // src/claude-session-wrapper.ts
10117
10331
  import { EventEmitter as EventEmitter4 } from "events";
10118
10332
  import { randomUUID as randomUUID5 } from "crypto";
10119
- import { existsSync as existsSync4, writeFileSync as writeFileSync5 } from "fs";
10333
+ import { existsSync as existsSync4, writeFileSync as writeFileSync6 } from "fs";
10120
10334
  import { unlink as unlink2 } from "fs/promises";
10121
- import { join as join11 } from "path";
10335
+ import { join as join13 } from "path";
10122
10336
 
10123
10337
  // src/claude-session-bun.ts
10124
10338
  import * as os5 from "os";
@@ -11299,8 +11513,8 @@ ${text2}`
11299
11513
  }
11300
11514
  function stageImage(data, extension) {
11301
11515
  try {
11302
- const file = join11(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
11303
- writeFileSync5(file, data, { mode: 384, flag: "wx" });
11516
+ const file = join13(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
11517
+ writeFileSync6(file, data, { mode: 384, flag: "wx" });
11304
11518
  return file;
11305
11519
  } catch (err) {
11306
11520
  log.warn("interactive transport could not stage an image", {
@@ -11841,13 +12055,13 @@ function dedupe2(dirs) {
11841
12055
  }
11842
12056
 
11843
12057
  // src/prompts.ts
11844
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
11845
- import { homedir as homedir9 } from "os";
12058
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
12059
+ import { homedir as homedir10 } from "os";
11846
12060
  import { randomUUID as randomUUID7 } from "crypto";
11847
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join12 } from "path";
12061
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join14 } from "path";
11848
12062
  function readPromptFileIfPresent(path15) {
11849
12063
  try {
11850
- const content = readFileSync7(path15, "utf8").trim();
12064
+ const content = readFileSync8(path15, "utf8").trim();
11851
12065
  return content || void 0;
11852
12066
  } catch {
11853
12067
  return void 0;
@@ -11856,7 +12070,7 @@ function readPromptFileIfPresent(path15) {
11856
12070
  function nearestWorkspaceAgentsPrompt(cwd) {
11857
12071
  let dir = cwd;
11858
12072
  while (true) {
11859
- const content = readPromptFileIfPresent(join12(dir, "AGENTS.md"));
12073
+ const content = readPromptFileIfPresent(join14(dir, "AGENTS.md"));
11860
12074
  if (content) return content;
11861
12075
  const parent = dirname6(dir);
11862
12076
  if (parent === dir) return void 0;
@@ -11952,9 +12166,9 @@ function extractSystemMessages(prompt) {
11952
12166
  }
11953
12167
  var CLAUDE_NATIVE_INSTRUCTION_FILES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md"]);
11954
12168
  function userAuthoredInstructions(forwarded, opts = {}) {
11955
- const readFile2 = opts.readFile ?? ((path15) => {
12169
+ const readFile3 = opts.readFile ?? ((path15) => {
11956
12170
  try {
11957
- return readFileSync7(path15, "utf8");
12171
+ return readFileSync8(path15, "utf8");
11958
12172
  } catch {
11959
12173
  return null;
11960
12174
  }
@@ -11970,7 +12184,7 @@ function userAuthoredInstructions(forwarded, opts = {}) {
11970
12184
  if (seen.has(path15) || !isAbsolute3(path15)) continue;
11971
12185
  if (CLAUDE_NATIVE_INSTRUCTION_FILES.has(basename2(path15))) continue;
11972
12186
  seen.add(path15);
11973
- const file = readFile2(path15);
12187
+ const file = readFile3(path15);
11974
12188
  if (!file?.trim()) continue;
11975
12189
  const start = match.index + match[0].length + 1;
11976
12190
  const content = forwarded.startsWith(file, start) ? file : forwarded.startsWith(file.trimEnd(), start) ? file.trimEnd() : null;
@@ -11995,8 +12209,8 @@ ${options.compressionSummary.trim()}`
11995
12209
  for (const s of extraSystemContent) {
11996
12210
  if (s.trim()) parts.push(s.trim());
11997
12211
  }
11998
- const configRoot = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
11999
- const globalAgents = readPromptFileIfPresent(join12(configRoot, "opencode", "AGENTS.md"));
12212
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
12213
+ const globalAgents = readPromptFileIfPresent(join14(configRoot, "opencode", "AGENTS.md"));
12000
12214
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
12001
12215
  const forwarded = extraSystemContent.join("\n\n");
12002
12216
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -12012,9 +12226,9 @@ ${options.compressionSummary.trim()}`
12012
12226
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
12013
12227
  const content = parts.join("\n\n");
12014
12228
  if (!content) return void 0;
12015
- const path15 = join12(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
12229
+ const path15 = join14(pluginTmpDir(), `opencode-cc-sys-${randomUUID7()}.md`);
12016
12230
  try {
12017
- writeFileSync6(path15, content, { encoding: "utf8", mode: 384 });
12231
+ writeFileSync7(path15, content, { encoding: "utf8", mode: 384 });
12018
12232
  return path15;
12019
12233
  } catch (err) {
12020
12234
  log.warn("failed to write system prompt file", { error: String(err) });
@@ -15346,7 +15560,7 @@ var ClaudeCodeLanguageModel = class {
15346
15560
  let replayReason = "no-session-for-key";
15347
15561
  if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
15348
15562
  const configDir = resolveConfigDir(this.config.configDir);
15349
- const transcriptPath2 = (id, dir) => join13(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
15563
+ const transcriptPath2 = (id, dir) => join15(dir ?? configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
15350
15564
  const resumePoint = findResumePoint({
15351
15565
  sessionKey: sk,
15352
15566
  prompt: options.prompt,
@@ -16693,7 +16907,7 @@ var ClaudeCodeLanguageModel = class {
16693
16907
  };
16694
16908
 
16695
16909
  // src/account-tier.ts
16696
- import { readFileSync as readFileSync8 } from "fs";
16910
+ import { readFileSync as readFileSync9 } from "fs";
16697
16911
  import path13 from "path";
16698
16912
  var ORGANIZATION_TYPE_LABELS = {
16699
16913
  claude_max: "Max",
@@ -16736,7 +16950,7 @@ function readTierFromConfigDir(configDir, allowLegacyFallback) {
16736
16950
  function readOauthAccount(file) {
16737
16951
  let raw;
16738
16952
  try {
16739
- raw = readFileSync8(file, "utf8");
16953
+ raw = readFileSync9(file, "utf8");
16740
16954
  } catch {
16741
16955
  return void 0;
16742
16956
  }
@@ -16764,14 +16978,14 @@ function tierLabel(oauthAccount) {
16764
16978
  // src/cleanup-stale.ts
16765
16979
  import {
16766
16980
  existsSync as existsSync5,
16767
- mkdirSync as mkdirSync5,
16768
- readFileSync as readFileSync9,
16981
+ mkdirSync as mkdirSync6,
16982
+ readFileSync as readFileSync10,
16769
16983
  realpathSync as realpathSync4,
16770
16984
  rmSync as rmSync3,
16771
- writeFileSync as writeFileSync7
16985
+ writeFileSync as writeFileSync8
16772
16986
  } from "fs";
16773
- import { homedir as homedir10 } from "os";
16774
- import { dirname as dirname7, join as join14, resolve as resolve5 } from "path";
16987
+ import { homedir as homedir11 } from "os";
16988
+ import { dirname as dirname7, join as join16, resolve as resolve5 } from "path";
16775
16989
  import { fileURLToPath as fileURLToPath4 } from "url";
16776
16990
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
16777
16991
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
@@ -16779,20 +16993,20 @@ var alreadyRan = false;
16779
16993
  function candidateCacheRoots() {
16780
16994
  const xdg = process.env.XDG_CACHE_HOME;
16781
16995
  return [
16782
- xdg ? join14(xdg, "opencode") : null,
16783
- join14(homedir10(), ".cache", "opencode"),
16784
- join14(homedir10(), "Library", "Caches", "opencode")
16996
+ xdg ? join16(xdg, "opencode") : null,
16997
+ join16(homedir11(), ".cache", "opencode"),
16998
+ join16(homedir11(), "Library", "Caches", "opencode")
16785
16999
  ].filter((p) => Boolean(p));
16786
17000
  }
16787
17001
  function userOpencodeJsonPath() {
16788
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join14(homedir10(), ".config");
16789
- return join14(xdgConfig, "opencode", "opencode.json");
17002
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join16(homedir11(), ".config");
17003
+ return join16(xdgConfig, "opencode", "opencode.json");
16790
17004
  }
16791
17005
  function userIntendsToUseUnscoped() {
16792
17006
  const cfg = userOpencodeJsonPath();
16793
17007
  if (!existsSync5(cfg)) return false;
16794
17008
  try {
16795
- const json = JSON.parse(readFileSync9(cfg, "utf8"));
17009
+ const json = JSON.parse(readFileSync10(cfg, "utf8"));
16796
17010
  const plugins = json.plugin;
16797
17011
  if (!Array.isArray(plugins)) return false;
16798
17012
  return plugins.some(
@@ -16813,19 +17027,19 @@ function ourLoadedDir() {
16813
17027
  function ourVersion(dir) {
16814
17028
  if (!dir) return null;
16815
17029
  try {
16816
- const pkg = JSON.parse(readFileSync9(join14(dir, "package.json"), "utf8"));
17030
+ const pkg = JSON.parse(readFileSync10(join16(dir, "package.json"), "utf8"));
16817
17031
  return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
16818
17032
  } catch {
16819
17033
  return null;
16820
17034
  }
16821
17035
  }
16822
17036
  function markerPath() {
16823
- const stateRoot = process.env.XDG_STATE_HOME ?? join14(homedir10(), ".local", "state");
16824
- return join14(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
17037
+ const stateRoot = process.env.XDG_STATE_HOME ?? join16(homedir11(), ".local", "state");
17038
+ return join16(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
16825
17039
  }
16826
17040
  function markerVersion() {
16827
17041
  try {
16828
- const marker = JSON.parse(readFileSync9(markerPath(), "utf8"));
17042
+ const marker = JSON.parse(readFileSync10(markerPath(), "utf8"));
16829
17043
  return typeof marker.version === "string" ? marker.version : null;
16830
17044
  } catch {
16831
17045
  return null;
@@ -16834,8 +17048,8 @@ function markerVersion() {
16834
17048
  function writeMarker(version) {
16835
17049
  const file = markerPath();
16836
17050
  try {
16837
- mkdirSync5(dirname7(file), { recursive: true });
16838
- writeFileSync7(
17051
+ mkdirSync6(dirname7(file), { recursive: true });
17052
+ writeFileSync8(
16839
17053
  file,
16840
17054
  JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
16841
17055
  "utf8"
@@ -16870,7 +17084,7 @@ function cleanupStaleUnscopedInstall() {
16870
17084
  }
16871
17085
  function cleanupOne(cacheRoot, ourDir) {
16872
17086
  if (!existsSync5(cacheRoot)) return;
16873
- const stalePath = join14(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
17087
+ const stalePath = join16(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
16874
17088
  if (!existsSync5(stalePath)) return;
16875
17089
  let realStalePath = stalePath;
16876
17090
  try {
@@ -16878,11 +17092,11 @@ function cleanupOne(cacheRoot, ourDir) {
16878
17092
  } catch {
16879
17093
  }
16880
17094
  if (ourDir && realStalePath === ourDir) return;
16881
- const pkgJsonPath = join14(stalePath, "package.json");
17095
+ const pkgJsonPath = join16(stalePath, "package.json");
16882
17096
  if (!existsSync5(pkgJsonPath)) return;
16883
17097
  let pkg = {};
16884
17098
  try {
16885
- pkg = JSON.parse(readFileSync9(pkgJsonPath, "utf8"));
17099
+ pkg = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
16886
17100
  } catch {
16887
17101
  return;
16888
17102
  }
@@ -16898,13 +17112,13 @@ function cleanupOne(cacheRoot, ourDir) {
16898
17112
  });
16899
17113
  return;
16900
17114
  }
16901
- const cachePkgJson = join14(cacheRoot, "package.json");
17115
+ const cachePkgJson = join16(cacheRoot, "package.json");
16902
17116
  if (!existsSync5(cachePkgJson)) return;
16903
17117
  try {
16904
- const cfg = JSON.parse(readFileSync9(cachePkgJson, "utf8"));
17118
+ const cfg = JSON.parse(readFileSync10(cachePkgJson, "utf8"));
16905
17119
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
16906
17120
  delete cfg.dependencies[STALE_PACKAGE_NAME];
16907
- writeFileSync7(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
17121
+ writeFileSync8(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
16908
17122
  log.info("cleanup-stale: pruned dep from cache package.json");
16909
17123
  }
16910
17124
  } catch (err) {