openmausbot 0.1.65 → 0.1.66
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/assets/index-BftlaHAF.css +1 -0
- package/dist/assets/index-DuR3H-Hf.js +278 -0
- package/dist/assets/{index-BZeH50da.js → index-kW5L1QeO.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +121 -0
- package/dist-server/container-mcp.js +1 -0
- package/dist-server/index.js +14824 -13321
- package/dist-server/openmausbot.js +358 -87
- package/dist-server/pair-cli.js +358 -87
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/attachments.js +125 -13
- package/dist-server/server/browser-engine-release.js +10 -8
- package/dist-server/server/browser-engine.js +226 -26
- package/dist-server/server/browser-live.js +567 -0
- package/dist-server/server/browser-proxy.js +129 -0
- package/dist-server/server/browser-runtime.js +370 -0
- package/dist-server/server/claude-accounts.js +103 -0
- package/dist-server/server/config.js +26 -19
- package/dist-server/server/custom-domain.js +14 -0
- package/dist-server/server/drivers/claude.js +77 -19
- package/dist-server/server/harness/bus.js +9 -4
- package/dist-server/server/harness/registry.js +8 -0
- package/dist-server/server/index.js +345 -72
- package/dist-server/server/provider-auth-sessions.js +7 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/src/lib/composer-attachments.js +3 -3
- package/dist-server/vps-container-mcp.js +1 -0
- package/package.json +1 -1
- package/dist/assets/index-BOrNJcfG.js +0 -273
- package/dist/assets/index-Dwzk199I.css +0 -1
|
@@ -513,7 +513,7 @@ export const PROVIDER_CREDENTIAL_ENV = [
|
|
|
513
513
|
];
|
|
514
514
|
/** Merge a partial config into ~/.openmausbot/config.json (secrets never
|
|
515
515
|
* echoed back — callers report configured-or-not booleans only). */
|
|
516
|
-
export function saveConfig(patch) {
|
|
516
|
+
export function saveConfig(patch, options = {}) {
|
|
517
517
|
const p = join(DATA_DIR, "config.json");
|
|
518
518
|
let disk = {};
|
|
519
519
|
try {
|
|
@@ -577,10 +577,17 @@ export function saveConfig(patch) {
|
|
|
577
577
|
}
|
|
578
578
|
if (checkedPatch.instances) {
|
|
579
579
|
const currentInstances = jsonObjectSchema.safeParse(disk.instances);
|
|
580
|
-
const
|
|
580
|
+
const storedInstances = currentInstances.success ? currentInstances.data : {};
|
|
581
|
+
const diskInstances = options.replaceInstances ? {} : storedInstances;
|
|
581
582
|
for (const [instanceId, entry] of Object.entries(checkedPatch.instances)) {
|
|
582
|
-
const current = jsonObjectSchema.safeParse(
|
|
583
|
+
const current = jsonObjectSchema.safeParse(storedInstances[instanceId]);
|
|
583
584
|
const merged = current.success ? { ...current.data } : {};
|
|
585
|
+
// Replacement clears omitted known settings, but retained shadow
|
|
586
|
+
// entries keep fields understood only by a newer app or driver.
|
|
587
|
+
if (options.replaceInstances) {
|
|
588
|
+
for (const key of Object.keys(instanceConfigSchema.shape))
|
|
589
|
+
delete merged[key];
|
|
590
|
+
}
|
|
584
591
|
Object.assign(merged, entry);
|
|
585
592
|
diskInstances[instanceId] = merged;
|
|
586
593
|
}
|
|
@@ -594,13 +601,13 @@ export function saveConfig(patch) {
|
|
|
594
601
|
* entry rides driver.defaultConfig(). Returns false for unknown instances
|
|
595
602
|
* when the fleet is explicitly configured. The returned map must stay
|
|
596
603
|
* PERSISTABLE: instanceConfigs() injects credential env into consuming
|
|
597
|
-
* drivers' entries for the live fleet, so
|
|
598
|
-
*
|
|
604
|
+
* drivers' entries for the live fleet, so only their originally configured
|
|
605
|
+
* environment is retained — otherwise saving an override would
|
|
599
606
|
* copy xai/box/opencodeGo secrets into the instances section of
|
|
600
607
|
* config.json. */
|
|
601
608
|
export function withInstanceCli(cfg, instanceId, cli) {
|
|
602
609
|
const next = structuredClone(cfg);
|
|
603
|
-
const map =
|
|
610
|
+
const map = persistableInstanceConfigs(next);
|
|
604
611
|
// hasOwn, not truthiness: map is a plain object literal, so
|
|
605
612
|
// map["__proto__"] resolves to Object.prototype — truthy — and the
|
|
606
613
|
// assignment below would poison EVERY object in the process (instanceId
|
|
@@ -620,22 +627,22 @@ export function withInstanceCli(cfg, instanceId, cli) {
|
|
|
620
627
|
delete rest.cli;
|
|
621
628
|
entry.config = Object.keys(rest).length ? rest : undefined;
|
|
622
629
|
}
|
|
623
|
-
for (const e of Object.values(map)) {
|
|
624
|
-
if (!e.environment)
|
|
625
|
-
continue;
|
|
626
|
-
const injected = injectedEnvironment(next, e.driver);
|
|
627
|
-
for (const [k, v] of Object.entries(e.environment)) {
|
|
628
|
-
if (injected.get(k) === v)
|
|
629
|
-
delete e.environment[k];
|
|
630
|
-
}
|
|
631
|
-
if (!Object.keys(e.environment).length)
|
|
632
|
-
delete e.environment;
|
|
633
|
-
}
|
|
634
630
|
next.instances = map;
|
|
635
631
|
return { ok: true, config: next };
|
|
636
632
|
}
|
|
637
|
-
/**
|
|
638
|
-
|
|
633
|
+
/** Materialize defaults without copying injected workspace secrets to disk. */
|
|
634
|
+
export function persistableInstanceConfigs(cfg) {
|
|
635
|
+
const map = instanceConfigs(cfg);
|
|
636
|
+
for (const [id, entry] of Object.entries(map)) {
|
|
637
|
+
const environment = cfg.instances?.[id]?.environment;
|
|
638
|
+
if (environment)
|
|
639
|
+
entry.environment = { ...environment };
|
|
640
|
+
else
|
|
641
|
+
delete entry.environment;
|
|
642
|
+
}
|
|
643
|
+
return map;
|
|
644
|
+
}
|
|
645
|
+
/** The credential env instanceConfigs() injects for one driver at runtime.
|
|
639
646
|
* Each secret goes only to the driver that actually reads it: the API-key
|
|
640
647
|
* Grok driver reads XAI_API_KEY, the Computer driver reads BOX_TOKEN, and
|
|
641
648
|
* OpenCode reads OPENCODE_API_KEY. Every other engine brings its own
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
3
3
|
import { request as httpsRequest } from "node:https";
|
|
4
4
|
import { BlockList, isIP } from "node:net";
|
|
5
|
+
import { networkInterfaces } from "node:os";
|
|
5
6
|
const CHECK_TIMEOUT_MS = 8_000;
|
|
6
7
|
const MAX_RESPONSE_BYTES = 16_384;
|
|
7
8
|
const DESCRIPTOR_PATH = "/.well-known/openmausbot/environment";
|
|
@@ -57,6 +58,19 @@ export function isPublicDomainAddress(address) {
|
|
|
57
58
|
return !excludedAddresses.check(address, "ipv4");
|
|
58
59
|
return family === 6 && globalIpv6.check(address, "ipv6") && !excludedAddresses.check(address, "ipv6");
|
|
59
60
|
}
|
|
61
|
+
/** Local interfaces only: a tunnel/CDN address or an external IP-echo service
|
|
62
|
+
* does not identify the server where the customer's HTTPS proxy runs. */
|
|
63
|
+
export function customDomainIpv4(interfaces = networkInterfaces(), configured = process.env.OMB_PUBLIC_IPV4) {
|
|
64
|
+
if (configured?.trim()) {
|
|
65
|
+
const address = configured.trim();
|
|
66
|
+
return isIP(address) === 4 && isPublicDomainAddress(address) ? address : null;
|
|
67
|
+
}
|
|
68
|
+
const addresses = new Set(Object.values(interfaces).flatMap((entries) => entries ?? [])
|
|
69
|
+
.filter((entry) => !entry.internal && entry.family === "IPv4" && isPublicDomainAddress(entry.address))
|
|
70
|
+
.map((entry) => entry.address));
|
|
71
|
+
// Containers/NAT and multi-address hosts need an explicit operator choice.
|
|
72
|
+
return addresses.size === 1 ? [...addresses][0] : null;
|
|
73
|
+
}
|
|
60
74
|
function withAbort(promise, signal) {
|
|
61
75
|
return new Promise((resolve, reject) => {
|
|
62
76
|
const abort = () => reject(new CustomDomainError("The domain check timed out. Check DNS, HTTPS, and your reverse proxy, then try again."));
|
|
@@ -12,7 +12,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
12
12
|
import { chmodSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { createServer as createNetServer } from "node:net";
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
|
-
import { join, dirname } from "node:path";
|
|
15
|
+
import { join, dirname, isAbsolute, normalize } from "node:path";
|
|
16
16
|
import { DATA_DIR, stripWorkspaceCredentialEnv } from "../config.js";
|
|
17
17
|
import { augmentedPath } from "../env-path.js";
|
|
18
18
|
import { brokerSocketPath, describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
@@ -29,19 +29,61 @@ import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
|
29
29
|
* backends over time. Presence checks also accept stale credentials. The CLI's
|
|
30
30
|
* own machine-readable auth command is the source of truth for every backend.
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
function claudeAuthStatus(cli, env, run = execCli) {
|
|
33
33
|
return new Promise((resolve) => {
|
|
34
|
-
run(cli, ["auth", "status", "--json"], { timeout: 8000, env }, (_error, stdout) => {
|
|
34
|
+
run(cli, ["auth", "status", "--json"], { timeout: 8000, maxBuffer: 65_536, env }, (_error, stdout) => {
|
|
35
35
|
try {
|
|
36
36
|
const status = JSON.parse(stdout);
|
|
37
|
-
|
|
37
|
+
if (!status || typeof status !== "object" || !("loggedIn" in status) || status.loggedIn !== true) {
|
|
38
|
+
return resolve({ authenticated: false });
|
|
39
|
+
}
|
|
40
|
+
// Only display identity fields, never the CLI's full auth response.
|
|
41
|
+
const identity = status;
|
|
42
|
+
const boundedText = (value, max) => typeof value === "string" && value.trim().length > 0 && value.length <= max && !/[\p{Cc}\p{Cf}]/u.test(value)
|
|
43
|
+
? value.trim() : undefined;
|
|
44
|
+
const candidateEmail = boundedText(identity.email, 254);
|
|
45
|
+
const email = candidateEmail && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(candidateEmail) ? candidateEmail : undefined;
|
|
46
|
+
const organization = boundedText(identity.orgName, 160);
|
|
47
|
+
resolve({
|
|
48
|
+
authenticated: true,
|
|
49
|
+
...(email || organization ? { account: { ...(email ? { email } : {}), ...(organization ? { organization } : {}) } } : {}),
|
|
50
|
+
});
|
|
38
51
|
}
|
|
39
52
|
catch {
|
|
40
|
-
resolve(false);
|
|
53
|
+
resolve({ authenticated: false });
|
|
41
54
|
}
|
|
42
55
|
});
|
|
43
56
|
});
|
|
44
57
|
}
|
|
58
|
+
export async function claudeSignedIn(cli, env, run = execCli) {
|
|
59
|
+
return (await claudeAuthStatus(cli, env, run)).authenticated;
|
|
60
|
+
}
|
|
61
|
+
/** Parent-session credentials/routing that a named account must not inherit.
|
|
62
|
+
* Shared with terminal sign-in instructions so login and turns select alike. */
|
|
63
|
+
export const CLAUDE_ACCOUNT_ENV_KEYS = [
|
|
64
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
65
|
+
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
66
|
+
"CLAUDE_CODE_OAUTH_SCOPES",
|
|
67
|
+
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
|
68
|
+
"CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
|
|
69
|
+
"ANTHROPIC_API_KEY",
|
|
70
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
71
|
+
"ANTHROPIC_BASE_URL",
|
|
72
|
+
"ANTHROPIC_CUSTOM_HEADERS",
|
|
73
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
74
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
75
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
76
|
+
];
|
|
77
|
+
/** Resolve the CLI's config location without changing HOME or Keychain. */
|
|
78
|
+
export function resolveClaudeConfigDir(configDir, env = process.env) {
|
|
79
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
80
|
+
const configured = configDir?.trim() || env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
81
|
+
const expanded = configured === "~" ? home : configured.startsWith("~/") ? join(home, configured.slice(2)) : configured;
|
|
82
|
+
if (!isAbsolute(expanded) || /[\p{Cc}\p{Cf}]/u.test(expanded)) {
|
|
83
|
+
throw new Error("claude: configDir must be an absolute path or start with ~/");
|
|
84
|
+
}
|
|
85
|
+
return normalize(expanded);
|
|
86
|
+
}
|
|
45
87
|
/** Whether a stream frame is the CLI reporting that it has no login.
|
|
46
88
|
*
|
|
47
89
|
* The CLI flags its own api-error frames (`error`, `is_api_error_message`);
|
|
@@ -63,8 +105,21 @@ export function claudeAuthFailure(frame, text) {
|
|
|
63
105
|
* Keeping the probe and turn environments identical prevents setup from
|
|
64
106
|
* claiming an API-key login that the turn itself would deliberately remove.
|
|
65
107
|
*/
|
|
66
|
-
function claudeEnvironment(model, source = process.env) {
|
|
108
|
+
function claudeEnvironment(model, source = process.env, configDir, instanceEnvironment = {}) {
|
|
67
109
|
const env = { ...source, PATH: augmentedPath(), NPM_CONFIG_LOGLEVEL: "error" };
|
|
110
|
+
if (configDir?.trim()) {
|
|
111
|
+
env.CLAUDE_CONFIG_DIR = resolveClaudeConfigDir(configDir, env);
|
|
112
|
+
for (const key of CLAUDE_ACCOUNT_ENV_KEYS) {
|
|
113
|
+
// Explicit custom endpoint settings still work; subscription OAuth
|
|
114
|
+
// always belongs to this account's CLI-managed login, never its parent.
|
|
115
|
+
if (key.startsWith("CLAUDE_CODE_OAUTH_") || key.endsWith("_FILE_DESCRIPTOR") || !Object.hasOwn(instanceEnvironment, key)) {
|
|
116
|
+
delete env[key];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else if (env.CLAUDE_CONFIG_DIR) {
|
|
121
|
+
env.CLAUDE_CONFIG_DIR = resolveClaudeConfigDir(undefined, env);
|
|
122
|
+
}
|
|
68
123
|
delete env.CLAUDECODE;
|
|
69
124
|
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
70
125
|
// The harness process may hold workspace credentials (xai/box/voice keys,
|
|
@@ -97,11 +152,6 @@ async function resolveClaudeTurnModel(model, env) {
|
|
|
97
152
|
}
|
|
98
153
|
return resolveInjectId(model, await probeLocalInjects(env)) ?? model;
|
|
99
154
|
}
|
|
100
|
-
function claudeConfigDir(env) {
|
|
101
|
-
if (env.CLAUDE_CONFIG_DIR)
|
|
102
|
-
return env.CLAUDE_CONFIG_DIR;
|
|
103
|
-
return join(env.HOME || env.USERPROFILE || homedir(), ".claude");
|
|
104
|
-
}
|
|
105
155
|
function extrasFromUnknown(value) {
|
|
106
156
|
if (!Array.isArray(value))
|
|
107
157
|
return [];
|
|
@@ -127,7 +177,7 @@ function extrasFromUnknown(value) {
|
|
|
127
177
|
export function readClaudeModelCatalog(env = process.env) {
|
|
128
178
|
let settings = {};
|
|
129
179
|
try {
|
|
130
|
-
settings = JSON.parse(readFileSync(join(
|
|
180
|
+
settings = JSON.parse(readFileSync(join(resolveClaudeConfigDir(undefined, env), "settings.json"), "utf8"));
|
|
131
181
|
}
|
|
132
182
|
catch {
|
|
133
183
|
return STATIC_CLAUDE_MODELS;
|
|
@@ -446,8 +496,14 @@ function decodeConfig(raw) {
|
|
|
446
496
|
}
|
|
447
497
|
const tools = decodeToolList(o.tools, "tools");
|
|
448
498
|
const disallowedTools = decodeToolList(o.disallowedTools, "disallowedTools");
|
|
499
|
+
if (o.configDir !== undefined && typeof o.configDir !== "string")
|
|
500
|
+
throw new Error("claude: configDir must be a string");
|
|
501
|
+
const configDir = typeof o.configDir === "string" ? o.configDir.trim() : undefined;
|
|
502
|
+
if (configDir)
|
|
503
|
+
resolveClaudeConfigDir(configDir);
|
|
449
504
|
return {
|
|
450
505
|
cli: typeof o.cli === "string" ? o.cli : "claude",
|
|
506
|
+
...(configDir ? { configDir } : {}),
|
|
451
507
|
permissionMode: mode ?? "acceptEdits",
|
|
452
508
|
...(tools !== undefined ? { tools } : {}),
|
|
453
509
|
...(disallowedTools !== undefined ? { disallowedTools } : {}),
|
|
@@ -523,7 +579,8 @@ export const ClaudeDriver = {
|
|
|
523
579
|
defaultConfig: () => decodeConfig({}),
|
|
524
580
|
async create(input) {
|
|
525
581
|
const { instanceId, config } = input;
|
|
526
|
-
const
|
|
582
|
+
const environment = (model) => claudeEnvironment(model, { ...process.env, ...input.environment }, config.configDir, input.environment);
|
|
583
|
+
const catalogEnv = environment();
|
|
527
584
|
let models = STATIC_CLAUDE_MODELS;
|
|
528
585
|
const refreshModels = async () => {
|
|
529
586
|
try {
|
|
@@ -659,7 +716,7 @@ export const ClaudeDriver = {
|
|
|
659
716
|
if (config.disallowedTools?.length) {
|
|
660
717
|
args.push("--disallowedTools", config.disallowedTools.join(","));
|
|
661
718
|
}
|
|
662
|
-
const turnEnvironment =
|
|
719
|
+
const turnEnvironment = environment();
|
|
663
720
|
const turnModel = await resolveClaudeTurnModel(turn.model, turnEnvironment);
|
|
664
721
|
const injected = applyClaudeInject({ ...turnEnvironment }, turnModel);
|
|
665
722
|
if (injected.model)
|
|
@@ -762,7 +819,7 @@ export const ClaudeDriver = {
|
|
|
762
819
|
args.push("--mcp-config", mcpConfigPath);
|
|
763
820
|
args.push("--allowedTools", allowed.join(","));
|
|
764
821
|
}
|
|
765
|
-
const env =
|
|
822
|
+
const env = environment(turnModel);
|
|
766
823
|
// Our approvals and browser credentials expire at the user-turn
|
|
767
824
|
// boundary. Native background workers cannot outlive that boundary;
|
|
768
825
|
// parallel bot work must use the harness's durable delegate_bot path.
|
|
@@ -779,6 +836,7 @@ export const ClaudeDriver = {
|
|
|
779
836
|
cwd,
|
|
780
837
|
model: injected.model ?? null,
|
|
781
838
|
base: env.ANTHROPIC_BASE_URL ?? null,
|
|
839
|
+
configDir: env.CLAUDE_CONFIG_DIR ?? null,
|
|
782
840
|
});
|
|
783
841
|
// Reuse the live process when it is idle, unchanged, and is the session
|
|
784
842
|
// the harness wants resumed. Anything else: close it and spawn fresh
|
|
@@ -1230,17 +1288,17 @@ export const ClaudeDriver = {
|
|
|
1230
1288
|
return writeUser(s, threadId, claudeUserMessage(text, undefined));
|
|
1231
1289
|
};
|
|
1232
1290
|
const snapshot = async () => {
|
|
1233
|
-
const env =
|
|
1291
|
+
const env = environment();
|
|
1234
1292
|
const version = await new Promise((resolve) => {
|
|
1235
1293
|
execCli(config.cli, ["--version"], { timeout: 8000, env }, (err, stdout) => resolve(err ? null : stdout.trim()));
|
|
1236
1294
|
});
|
|
1237
1295
|
if (!version)
|
|
1238
1296
|
return { state: "unavailable", reason: `\`${config.cli}\` CLI not found` };
|
|
1239
|
-
const
|
|
1297
|
+
const auth = await claudeAuthStatus(config.cli, env);
|
|
1240
1298
|
// claudeEnvironment strips ANTHROPIC_API_KEY, so turns run on the
|
|
1241
1299
|
// CLI's own login (Pro/Max): the cost it reports is what the call
|
|
1242
1300
|
// WOULD bill, not a charge
|
|
1243
|
-
return { state: "available", version,
|
|
1301
|
+
return { state: "available", version, ...auth, billing: "subscription" };
|
|
1244
1302
|
};
|
|
1245
1303
|
/** One-shot Claude call with the prompt on stdin, never argv. Approval
|
|
1246
1304
|
* summaries can contain paths, commands, or secrets, so the generic
|
|
@@ -1249,7 +1307,7 @@ export const ClaudeDriver = {
|
|
|
1249
1307
|
const generateReview = (prompt, signal) => new Promise((resolve, reject) => {
|
|
1250
1308
|
const child = spawnCli(config.cli, ["-p", "--model", "claude-haiku-4-5", "--output-format", "text"], {
|
|
1251
1309
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1252
|
-
env:
|
|
1310
|
+
env: environment("claude-haiku-4-5"),
|
|
1253
1311
|
});
|
|
1254
1312
|
let stdout = "";
|
|
1255
1313
|
let stderr = "";
|
|
@@ -12,7 +12,7 @@ import { newId } from "../contracts.js";
|
|
|
12
12
|
const INCOMPLETE_LOG_MESSAGE = "Canonical event history is incomplete: OpenMausBot could not write one or more events to disk. Live updates will continue.";
|
|
13
13
|
export class EventBus {
|
|
14
14
|
listeners = new Set();
|
|
15
|
-
unsubscribes =
|
|
15
|
+
unsubscribes = new Map();
|
|
16
16
|
pendingLogWarnings = new Map();
|
|
17
17
|
appendLog;
|
|
18
18
|
constructor(appendLog = appendFileSync) {
|
|
@@ -20,6 +20,7 @@ export class EventBus {
|
|
|
20
20
|
}
|
|
21
21
|
attach(instances) {
|
|
22
22
|
for (const instance of instances) {
|
|
23
|
+
this.detach(instance.instanceId);
|
|
23
24
|
const unsub = instance.adapter.onEvent((event) => {
|
|
24
25
|
// hard invariant borrowed from correlateRuntimeEventWithInstance:
|
|
25
26
|
// an adapter may only emit events for its own driver kind
|
|
@@ -29,7 +30,7 @@ export class EventBus {
|
|
|
29
30
|
}
|
|
30
31
|
this.publish({ ...event, providerInstanceId: instance.instanceId });
|
|
31
32
|
});
|
|
32
|
-
this.unsubscribes.
|
|
33
|
+
this.unsubscribes.set(instance.instanceId, unsub);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
publish(event) {
|
|
@@ -80,7 +81,11 @@ export class EventBus {
|
|
|
80
81
|
return () => this.listeners.delete(listener);
|
|
81
82
|
}
|
|
82
83
|
detachAll() {
|
|
83
|
-
for (const
|
|
84
|
-
|
|
84
|
+
for (const id of this.unsubscribes.keys())
|
|
85
|
+
this.detach(id);
|
|
86
|
+
}
|
|
87
|
+
detach(instanceId) {
|
|
88
|
+
this.unsubscribes.get(instanceId)?.();
|
|
89
|
+
this.unsubscribes.delete(instanceId);
|
|
85
90
|
}
|
|
86
91
|
}
|
|
@@ -35,6 +35,8 @@ export class ProviderRegistry {
|
|
|
35
35
|
}
|
|
36
36
|
async load(configs) {
|
|
37
37
|
for (const [instanceId, entry] of Object.entries(configs)) {
|
|
38
|
+
// Account edits replace only their own process/session state.
|
|
39
|
+
await this.dispose(instanceId);
|
|
38
40
|
const driver = this.driversByKind.get(entry.driver);
|
|
39
41
|
if (!driver) {
|
|
40
42
|
this.byId.set(instanceId, {
|
|
@@ -221,4 +223,10 @@ export class ProviderRegistry {
|
|
|
221
223
|
this.byId.clear();
|
|
222
224
|
this.cliByInstance.clear();
|
|
223
225
|
}
|
|
226
|
+
async dispose(instanceId) {
|
|
227
|
+
const entry = this.byId.get(instanceId);
|
|
228
|
+
this.byId.delete(instanceId);
|
|
229
|
+
this.cliByInstance.delete(instanceId);
|
|
230
|
+
await entry?.live?.dispose();
|
|
231
|
+
}
|
|
224
232
|
}
|