@0xmaxma/claude-gateway 1.7.10 → 1.8.1
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/README.md +167 -41
- package/dist/agent/builtin-commands.js +2 -2
- package/dist/agent/builtin-commands.js.map +1 -1
- package/dist/agent/knowledge/config.d.ts.map +1 -1
- package/dist/agent/knowledge/config.js +2 -9
- package/dist/agent/knowledge/config.js.map +1 -1
- package/dist/agent/model-catalog.d.ts +62 -0
- package/dist/agent/model-catalog.d.ts.map +1 -0
- package/dist/agent/model-catalog.js +304 -0
- package/dist/agent/model-catalog.js.map +1 -0
- package/dist/agent/runner.d.ts +76 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +212 -41
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/cron-router.d.ts +12 -8
- package/dist/api/cron-router.d.ts.map +1 -1
- package/dist/api/cron-router.js +82 -16
- package/dist/api/cron-router.js.map +1 -1
- package/dist/api/gateway-router.d.ts +4 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +14 -0
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/manifest-sources.d.ts +15 -0
- package/dist/api/manifest-sources.d.ts.map +1 -0
- package/dist/api/manifest-sources.js +27 -0
- package/dist/api/manifest-sources.js.map +1 -0
- package/dist/api/meta-router.d.ts +13 -0
- package/dist/api/meta-router.d.ts.map +1 -0
- package/dist/api/meta-router.js +26 -0
- package/dist/api/meta-router.js.map +1 -0
- package/dist/api/packages.d.ts.map +1 -1
- package/dist/api/packages.js +18 -198
- package/dist/api/packages.js.map +1 -1
- package/dist/api/route-registry.d.ts +81 -0
- package/dist/api/route-registry.d.ts.map +1 -0
- package/dist/api/route-registry.js +44 -0
- package/dist/api/route-registry.js.map +1 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +21 -3
- package/dist/api/router.js.map +1 -1
- package/dist/apps/installer.d.ts +73 -5
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +237 -22
- package/dist/apps/installer.js.map +1 -1
- package/dist/cli/args.d.ts +25 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +55 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/colors.d.ts +37 -0
- package/dist/cli/colors.d.ts.map +1 -0
- package/dist/cli/colors.js +63 -0
- package/dist/cli/colors.js.map +1 -0
- package/dist/cli/command-names.d.ts +98 -0
- package/dist/cli/command-names.d.ts.map +1 -0
- package/dist/cli/command-names.js +109 -0
- package/dist/cli/command-names.js.map +1 -0
- package/dist/cli/commands/agents.d.ts +28 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/agents.js +321 -0
- package/dist/cli/commands/agents.js.map +1 -0
- package/dist/cli/commands/channels.d.ts +17 -0
- package/dist/cli/commands/channels.d.ts.map +1 -0
- package/dist/cli/commands/channels.js +101 -0
- package/dist/cli/commands/channels.js.map +1 -0
- package/dist/cli/commands/debug-bundle.d.ts +6 -0
- package/dist/cli/commands/debug-bundle.d.ts.map +1 -0
- package/dist/cli/commands/debug-bundle.js +220 -0
- package/dist/cli/commands/debug-bundle.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +104 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/gateway.d.ts +12 -0
- package/dist/cli/commands/gateway.d.ts.map +1 -0
- package/dist/cli/commands/gateway.js +145 -0
- package/dist/cli/commands/gateway.js.map +1 -0
- package/dist/cli/commands/service.d.ts +33 -0
- package/dist/cli/commands/service.d.ts.map +1 -0
- package/dist/cli/commands/service.js +513 -0
- package/dist/cli/commands/service.js.map +1 -0
- package/dist/cli/commands/update.d.ts +16 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +149 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands.generated.d.ts +4 -0
- package/dist/cli/commands.generated.d.ts.map +1 -0
- package/dist/cli/commands.generated.js +220 -0
- package/dist/cli/commands.generated.js.map +1 -0
- package/dist/cli/health.d.ts +31 -0
- package/dist/cli/health.d.ts.map +1 -0
- package/dist/cli/health.js +49 -0
- package/dist/cli/health.js.map +1 -0
- package/dist/cli/http-client.d.ts +140 -0
- package/dist/cli/http-client.d.ts.map +1 -0
- package/dist/cli/http-client.js +332 -0
- package/dist/cli/http-client.js.map +1 -0
- package/dist/cli/index.d.ts +27 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +372 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/manager.d.ts +64 -0
- package/dist/cli/manager.d.ts.map +1 -0
- package/dist/cli/manager.js +181 -0
- package/dist/cli/manager.js.map +1 -0
- package/dist/cli/output.d.ts +49 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +101 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/prompt.d.ts +29 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +154 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/redact.d.ts +3 -0
- package/dist/cli/redact.d.ts.map +1 -0
- package/dist/cli/redact.js +33 -0
- package/dist/cli/redact.js.map +1 -0
- package/dist/cli/types.d.ts +22 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config/bootstrap.d.ts +33 -0
- package/dist/config/bootstrap.d.ts.map +1 -0
- package/dist/config/bootstrap.js +117 -0
- package/dist/config/bootstrap.js.map +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +5 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +2 -35
- package/dist/config/watcher.js.map +1 -1
- package/dist/discord/receiver.d.ts +6 -1
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +9 -1
- package/dist/discord/receiver.js.map +1 -1
- package/dist/entry.d.ts +3 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +83 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.js +160 -46
- package/dist/index.js.map +1 -1
- package/dist/load-dotenv.d.ts +2 -0
- package/dist/load-dotenv.d.ts.map +1 -0
- package/dist/load-dotenv.js +68 -0
- package/dist/load-dotenv.js.map +1 -0
- package/dist/packages/registry.d.ts +50 -0
- package/dist/packages/registry.d.ts.map +1 -0
- package/dist/packages/registry.js +209 -0
- package/dist/packages/registry.js.map +1 -0
- package/dist/session/process.d.ts +14 -5
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +155 -26
- package/dist/session/process.js.map +1 -1
- package/dist/shutdown-signals.d.ts +47 -0
- package/dist/shutdown-signals.d.ts.map +1 -0
- package/dist/shutdown-signals.js +62 -0
- package/dist/shutdown-signals.js.map +1 -0
- package/dist/telegram/receiver.d.ts +6 -1
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +9 -1
- package/dist/telegram/receiver.js.map +1 -1
- package/dist/utils/orphan-receivers.d.ts +90 -0
- package/dist/utils/orphan-receivers.d.ts.map +1 -0
- package/dist/utils/orphan-receivers.js +218 -0
- package/dist/utils/orphan-receivers.js.map +1 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +60 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/stop-child.d.ts +21 -0
- package/dist/utils/stop-child.d.ts.map +1 -0
- package/dist/utils/stop-child.js +64 -0
- package/dist/utils/stop-child.js.map +1 -0
- package/lib/pairing.ts +1 -1
- package/mcp/tools/agent/handlers.ts +1 -1
- package/mcp/tools/discord/receiver-server.ts +4 -0
- package/mcp/tools/telegram/receiver-server.ts +152 -29
- package/package.json +6 -4
package/dist/agent/runner.js
CHANGED
|
@@ -51,6 +51,7 @@ const slack_client_1 = require("../api/slack-client");
|
|
|
51
51
|
const markdown_1 = require("../telegram/markdown");
|
|
52
52
|
const skills_1 = require("../skills");
|
|
53
53
|
const builtin_commands_1 = require("./builtin-commands");
|
|
54
|
+
const model_catalog_1 = require("./model-catalog");
|
|
54
55
|
const safe_mode_1 = require("./safe-mode");
|
|
55
56
|
const recovery_executor_1 = require("./recovery-executor");
|
|
56
57
|
const recovery_policy_1 = require("./recovery-policy");
|
|
@@ -434,7 +435,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
434
435
|
// Check if this is a built-in channel command
|
|
435
436
|
const trimmedContent = content.trim();
|
|
436
437
|
if ((0, builtin_commands_1.isBuiltinCommand)(trimmedContent, channelSource)) {
|
|
437
|
-
this.handleSessionCommand(chatId, trimmedContent)
|
|
438
|
+
this.handleSessionCommand(chatId, trimmedContent, meta)
|
|
438
439
|
.then(() => this.writeTypingDone(chatId))
|
|
439
440
|
.catch((err) => {
|
|
440
441
|
this.logger.error('Session command failed', { error: err.message });
|
|
@@ -685,8 +686,24 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
685
686
|
return;
|
|
686
687
|
}
|
|
687
688
|
if (command === 'get_models') {
|
|
688
|
-
|
|
689
|
-
|
|
689
|
+
// Live catalog first, static list as the fallback (issue #409). The
|
|
690
|
+
// picker is a UI action, so a slow or unreachable catalog must not hang
|
|
691
|
+
// it — fetchModelCatalog is bounded and never rejects.
|
|
692
|
+
void this.availableModels()
|
|
693
|
+
.then((availableModels) => {
|
|
694
|
+
const configuredIds = new Set((this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS).map(m => m.id));
|
|
695
|
+
const configured = availableModels.filter(m => configuredIds.has(m.id));
|
|
696
|
+
const live = availableModels.filter(m => !configuredIds.has(m.id));
|
|
697
|
+
respond({
|
|
698
|
+
models: availableModels.map(m => ({ id: m.id, label: m.label })),
|
|
699
|
+
configuredModels: configured.map(m => ({ id: m.id, label: m.label })),
|
|
700
|
+
liveModels: live.map(m => ({ id: m.id, label: m.label })),
|
|
701
|
+
});
|
|
702
|
+
})
|
|
703
|
+
// Without this the request would hang and the picker with it: the
|
|
704
|
+
// receiver is blocked on this response. Every other async branch in
|
|
705
|
+
// this handler answers on rejection too.
|
|
706
|
+
.catch((err) => respond({ success: false, error: String(err) }, 500));
|
|
690
707
|
return;
|
|
691
708
|
}
|
|
692
709
|
// `/cli` webview terminal viewer — create a pairing on behalf of an
|
|
@@ -735,15 +752,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
735
752
|
respond({ success: false, error: err.message });
|
|
736
753
|
return;
|
|
737
754
|
}
|
|
738
|
-
|
|
739
|
-
const restartPromises = [];
|
|
740
|
-
for (const [key, session] of this.sessions) {
|
|
741
|
-
if (session.source !== 'api') {
|
|
742
|
-
restarted = true;
|
|
743
|
-
restartPromises.push(this.restartProcess(key));
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
await Promise.all(restartPromises);
|
|
755
|
+
const restarted = await this.restartChannelSessions();
|
|
747
756
|
respond({ success: true, model: newModel, restarted });
|
|
748
757
|
return;
|
|
749
758
|
}
|
|
@@ -788,9 +797,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
788
797
|
return respond({ success: true, text: 'No active session found.' });
|
|
789
798
|
}
|
|
790
799
|
const effectiveModel = this.agentConfig.claude.model;
|
|
791
|
-
const
|
|
792
|
-
const modelConfig = availableModels.find(m => m.id === effectiveModel);
|
|
793
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
800
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
794
801
|
const contextTokens = meta.lastInputTokens ?? 0;
|
|
795
802
|
const usedPct = Math.round((contextTokens / contextWindow) * 100);
|
|
796
803
|
let msgs;
|
|
@@ -1879,7 +1886,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1879
1886
|
/**
|
|
1880
1887
|
* Dispatch a session management command to the appropriate handler.
|
|
1881
1888
|
*/
|
|
1882
|
-
async handleSessionCommand(chatId, content) {
|
|
1889
|
+
async handleSessionCommand(chatId, content, meta = {}) {
|
|
1883
1890
|
const agentId = this.agentConfig.id;
|
|
1884
1891
|
if (content.startsWith('/sessions')) {
|
|
1885
1892
|
await this.handleCommandSessions(agentId, chatId);
|
|
@@ -1901,10 +1908,88 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1901
1908
|
const name = content.replace('/rename', '').trim();
|
|
1902
1909
|
await this.handleCommandRename(agentId, chatId, name);
|
|
1903
1910
|
}
|
|
1911
|
+
else if (content.startsWith('/models')) {
|
|
1912
|
+
await this.handleCommandModels(chatId);
|
|
1913
|
+
}
|
|
1914
|
+
else if (content.startsWith('/model')) {
|
|
1915
|
+
await this.handleCommandModel(chatId, content.replace('/model', '').trim(), meta);
|
|
1916
|
+
}
|
|
1904
1917
|
else if (content.startsWith('/stop')) {
|
|
1905
1918
|
await this.handleCommandStop(chatId);
|
|
1906
1919
|
}
|
|
1907
1920
|
}
|
|
1921
|
+
/**
|
|
1922
|
+
* /models — the model picker for channels without an inline keyboard.
|
|
1923
|
+
*
|
|
1924
|
+
* Telegram never reaches this: its receiver answers `/models` itself with a
|
|
1925
|
+
* button keyboard and does not forward the message. Discord and LINE have no
|
|
1926
|
+
* picker of their own, so they get the list as text plus the command that
|
|
1927
|
+
* selects from it (issue #409).
|
|
1928
|
+
*
|
|
1929
|
+
* A channel listed in BUILTIN_COMMANDS but missing a branch here is worse
|
|
1930
|
+
* than one not listed at all — the gate swallows the message and the user
|
|
1931
|
+
* gets silence instead of the agent's reply. `/model` on Discord was in
|
|
1932
|
+
* exactly that state before this.
|
|
1933
|
+
*/
|
|
1934
|
+
async handleCommandModels(chatId) {
|
|
1935
|
+
const models = await this.availableModels();
|
|
1936
|
+
const configuredIds = new Set((this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS).map(m => m.id));
|
|
1937
|
+
const configured = models.filter(m => configuredIds.has(m.id));
|
|
1938
|
+
const live = models.filter(m => !configuredIds.has(m.id));
|
|
1939
|
+
const current = this.agentConfig.claude.model;
|
|
1940
|
+
const lines = [`Current model: ${current}`, '', 'Configured models'];
|
|
1941
|
+
for (const m of configured) {
|
|
1942
|
+
const marker = m.id === current ? '✅ ' : '• ';
|
|
1943
|
+
lines.push(`${marker}${m.label} — \`${m.id}\``);
|
|
1944
|
+
}
|
|
1945
|
+
if (live.length) {
|
|
1946
|
+
lines.push('', 'More models (live)');
|
|
1947
|
+
for (const m of live) {
|
|
1948
|
+
const marker = m.id === current ? '✅ ' : '• ';
|
|
1949
|
+
lines.push(`${marker}${m.label} — \`${m.id}\``);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
lines.push('', 'Switch with /model <id or alias>');
|
|
1953
|
+
this.writeAutoForward(chatId, lines.join('\n'));
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* /model — show the current model, or switch to one from the catalog.
|
|
1957
|
+
*
|
|
1958
|
+
* An unrecognised argument is refused rather than persisted: unlike the API
|
|
1959
|
+
* (where the caller is authenticated and the provider validates the id), a
|
|
1960
|
+
* typo in a chat message would otherwise be written into config.json and
|
|
1961
|
+
* break the agent's next spawn. Anything the catalog knows — including a
|
|
1962
|
+
* model that exists only upstream and not in the static list — is accepted.
|
|
1963
|
+
*/
|
|
1964
|
+
async handleCommandModel(chatId, arg, meta = {}) {
|
|
1965
|
+
const current = this.agentConfig.claude.model;
|
|
1966
|
+
if (!arg) {
|
|
1967
|
+
this.writeAutoForward(chatId, `Current model: ${current}\n\nUse /models to see the list.`);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (!this.isDirectChat(meta)) {
|
|
1971
|
+
this.writeAutoForward(chatId, 'Switching models is only available in a direct message with the bot — '
|
|
1972
|
+
+ 'it changes the model for every chat of this agent and restarts them. '
|
|
1973
|
+
+ 'Use /models here to see the list.');
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const models = await this.selectableModels();
|
|
1977
|
+
const wanted = arg.toLowerCase();
|
|
1978
|
+
const match = models.find((m) => m.id.toLowerCase() === wanted || m.alias.toLowerCase() === wanted);
|
|
1979
|
+
if (!match) {
|
|
1980
|
+
this.writeAutoForward(chatId, `Unknown model "${arg}". Use /models to see the list.`);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if (match.id === current) {
|
|
1984
|
+
this.writeAutoForward(chatId, `Already using ${match.label} (${match.id}).`);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
await this.setModel(match.id);
|
|
1988
|
+
// Same restart the Telegram picker's set_model does — the running session
|
|
1989
|
+
// was spawned with the old model and would otherwise keep using it.
|
|
1990
|
+
const restarted = await this.restartChannelSessions();
|
|
1991
|
+
this.writeAutoForward(chatId, `Model set to ${match.label} (${match.id}).${restarted ? ' Restarting the session…' : ''}`);
|
|
1992
|
+
}
|
|
1908
1993
|
/**
|
|
1909
1994
|
* /sessions — list all sessions for this chat.
|
|
1910
1995
|
*/
|
|
@@ -1933,9 +2018,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1933
2018
|
return;
|
|
1934
2019
|
}
|
|
1935
2020
|
const effectiveModel = this.agentConfig.claude.model;
|
|
1936
|
-
const
|
|
1937
|
-
const modelConfig = availableModels.find(m => m.id === effectiveModel);
|
|
1938
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
2021
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
1939
2022
|
const contextTokens = meta.lastInputTokens ?? 0;
|
|
1940
2023
|
const usedPct = Math.round((contextTokens / contextWindow) * 100);
|
|
1941
2024
|
let msgs;
|
|
@@ -2053,9 +2136,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2053
2136
|
const meta = index.sessions.find(s => s.id === sessionId);
|
|
2054
2137
|
const name = meta?.name ?? 'Session';
|
|
2055
2138
|
const compactModel = this.agentConfig.claude.model;
|
|
2056
|
-
const
|
|
2057
|
-
const modelConfig = availableModels.find(m => m.id === compactModel);
|
|
2058
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
2139
|
+
const contextWindow = await this.contextWindowFor(compactModel);
|
|
2059
2140
|
this.writeAutoForward(chatId, `⏳ Compacting session "${name}"...`);
|
|
2060
2141
|
try {
|
|
2061
2142
|
const compactor = new compactor_1.SessionCompactor(this.sessionStore);
|
|
@@ -2263,9 +2344,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2263
2344
|
// bystander (mirrors the image-size-threshold path).
|
|
2264
2345
|
// - hasLikelyOutstandingBackgroundWork(): regardless of which tier
|
|
2265
2346
|
// called restartOrDefer — even today's aggressive default — a
|
|
2266
|
-
// session that dispatched a background Agent/Workflow
|
|
2267
|
-
// then ended its OWN turn looks idle here, but
|
|
2268
|
-
//
|
|
2347
|
+
// session that dispatched a background Agent/Workflow/Monitor
|
|
2348
|
+
// tool_use and then ended its OWN turn looks idle here, but that
|
|
2349
|
+
// dispatch may still be doing real work. SIGKILLing it now would
|
|
2269
2350
|
// silently destroy that in-flight work with no notification or
|
|
2270
2351
|
// recovery (the incident this guards against: a CLAUDE.md-triggered
|
|
2271
2352
|
// restart killed a session mid-way through 3 dispatched code-review
|
|
@@ -2470,8 +2551,9 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2470
2551
|
startIdleCleaner() {
|
|
2471
2552
|
this.idleCleanerTimer = setInterval(async () => {
|
|
2472
2553
|
for (const [id, proc] of this.sessions) {
|
|
2473
|
-
// A parent that just dispatched Agent/Workflow work is CLI-idle
|
|
2474
|
-
//
|
|
2554
|
+
// A parent that just dispatched Agent/Workflow/Monitor work is CLI-idle
|
|
2555
|
+
// but that dispatch may still need this session to receive its
|
|
2556
|
+
// eventual task notification.
|
|
2475
2557
|
if (proc.hasLikelyOutstandingBackgroundWork())
|
|
2476
2558
|
continue;
|
|
2477
2559
|
if (proc.isIdle(this.idleTimeoutMs)) {
|
|
@@ -2571,7 +2653,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2571
2653
|
stopTelegramReceiver() {
|
|
2572
2654
|
if (!this.receiver)
|
|
2573
2655
|
return;
|
|
2574
|
-
|
|
2656
|
+
// Hot-remove is a config-change response, not a shutdown: detach immediately
|
|
2657
|
+
// and let teardown finish in the background rather than blocking the caller
|
|
2658
|
+
// for the SIGKILL grace period. stop() never rejects.
|
|
2659
|
+
void this.receiver.stop();
|
|
2575
2660
|
this.receiver = null;
|
|
2576
2661
|
this.logger.info('TelegramReceiver stopped', { agentId: this.agentConfig.id });
|
|
2577
2662
|
}
|
|
@@ -2587,7 +2672,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2587
2672
|
stopDiscordReceiver() {
|
|
2588
2673
|
if (!this.discordReceiver)
|
|
2589
2674
|
return;
|
|
2590
|
-
|
|
2675
|
+
// Hot-remove is a config-change response, not a shutdown: detach immediately
|
|
2676
|
+
// and let teardown finish in the background rather than blocking the caller
|
|
2677
|
+
// for the SIGKILL grace period. stop() never rejects.
|
|
2678
|
+
void this.discordReceiver.stop();
|
|
2591
2679
|
this.discordReceiver = null;
|
|
2592
2680
|
this.logger.info('DiscordReceiver stopped', { agentId: this.agentConfig.id });
|
|
2593
2681
|
}
|
|
@@ -2609,10 +2697,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2609
2697
|
clearTimeout(buf.timer);
|
|
2610
2698
|
}
|
|
2611
2699
|
this.channelCoalesce.clear();
|
|
2612
|
-
|
|
2613
|
-
|
|
2700
|
+
// Receiver teardown now resolves only once the child has exited, so it must
|
|
2701
|
+
// be awaited or shutdown() can exit the gateway out from under a receiver
|
|
2702
|
+
// that is still shutting down (issue #405). Run it concurrently with session
|
|
2703
|
+
// teardown rather than serially — both are bounded, neither depends on the
|
|
2704
|
+
// other, and shutdown latency is user-visible.
|
|
2705
|
+
const receiversStopped = [this.receiver?.stop(), this.discordReceiver?.stop()];
|
|
2614
2706
|
this.stopLineReply();
|
|
2615
|
-
await Promise.all([
|
|
2707
|
+
await Promise.all([
|
|
2708
|
+
...receiversStopped,
|
|
2709
|
+
...[...this.sessions.values()].map((s) => s.stop()),
|
|
2710
|
+
]);
|
|
2616
2711
|
this.sessions.clear();
|
|
2617
2712
|
}
|
|
2618
2713
|
/**
|
|
@@ -3412,9 +3507,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3412
3507
|
responseText = `Session: (unnamed)\nMessages: ${messageCount}\nContext used: 0%\nModel: ${effectiveModel}`;
|
|
3413
3508
|
}
|
|
3414
3509
|
else {
|
|
3415
|
-
const
|
|
3416
|
-
const modelConfig = availableModels.find((m) => m.id === effectiveModel);
|
|
3417
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
3510
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
3418
3511
|
const contextUsedPct = Math.round(((meta.lastInputTokens ?? 0) / contextWindow) * 100);
|
|
3419
3512
|
result = { sessionId, sessionName: meta.name, messageCount, archivedCount: meta.archivedCount ?? 0, contextUsedPct, model: effectiveModel };
|
|
3420
3513
|
responseText = [
|
|
@@ -3471,9 +3564,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3471
3564
|
const ch = 'api';
|
|
3472
3565
|
const activeSession = this.sessions.get(sessionId);
|
|
3473
3566
|
const compactEffectiveModel = opts?.model ?? activeSession?.modelOverride ?? this.agentConfig.claude.model;
|
|
3474
|
-
const
|
|
3475
|
-
const modelConfig = availableModels.find((m) => m.id === compactEffectiveModel);
|
|
3476
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
3567
|
+
const contextWindow = await this.contextWindowFor(compactEffectiveModel);
|
|
3477
3568
|
const compactor = new compactor_1.SessionCompactor(this.sessionStore);
|
|
3478
3569
|
const compactResult = await compactor.compact(agentId, storeChatId, sessionId, compactEffectiveModel, contextWindow, ch);
|
|
3479
3570
|
await this.sessionStore.updateSessionMeta(agentId, storeChatId, sessionId, {
|
|
@@ -3507,6 +3598,88 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3507
3598
|
persist('assistant', responseText, forcePersist);
|
|
3508
3599
|
return { result, responseText };
|
|
3509
3600
|
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Whether an inbound channel message came from a 1:1 chat with the bot.
|
|
3603
|
+
*
|
|
3604
|
+
* Gates the one channel command that reaches past its own chat: `/model <x>`
|
|
3605
|
+
* rewrites `config.json` for the whole agent and restarts every session in
|
|
3606
|
+
* every chat. The channel access gates do not cover that. Discord's, in a
|
|
3607
|
+
* guild, checks the guild, the channel, and a mention — never the user
|
|
3608
|
+
* (`allowFrom` in mcp/tools/discord/access.ts is DM-only), so without this
|
|
3609
|
+
* any member of an allowlisted channel could switch the model out from under
|
|
3610
|
+
* everyone else and kill their in-flight turns. LINE forwards group and room
|
|
3611
|
+
* messages the same way. Telegram's receiver answers `/model` itself behind
|
|
3612
|
+
* a private-chat + allowlist check and does not forward it here.
|
|
3613
|
+
*
|
|
3614
|
+
* Fails closed: a channel that does not report a chat type cannot switch.
|
|
3615
|
+
* Listing models (`/models`, bare `/model`) is unaffected — it reads nothing
|
|
3616
|
+
* and changes nothing.
|
|
3617
|
+
*/
|
|
3618
|
+
isDirectChat(meta) {
|
|
3619
|
+
const kind = meta['chat_type'] ?? meta['line_chat_type'] ?? meta['slack_chat_type'];
|
|
3620
|
+
return kind === 'direct' || kind === 'user';
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* Restart every non-api session so a model change actually takes effect.
|
|
3624
|
+
*
|
|
3625
|
+
* `setModel` only rewrites config — a session process was spawned with the
|
|
3626
|
+
* old model on its command line and keeps using it until it is restarted.
|
|
3627
|
+
* Reporting "model set" without this is a false success: the next turn still
|
|
3628
|
+
* runs on the previous model. Returns whether anything was restarted.
|
|
3629
|
+
*/
|
|
3630
|
+
async restartChannelSessions() {
|
|
3631
|
+
const restartPromises = [];
|
|
3632
|
+
for (const [key, session] of this.sessions) {
|
|
3633
|
+
if (session.source !== 'api')
|
|
3634
|
+
restartPromises.push(this.restartProcess(key));
|
|
3635
|
+
}
|
|
3636
|
+
await Promise.all(restartPromises);
|
|
3637
|
+
return restartPromises.length > 0;
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* The model list this gateway offers: the live catalog when one is reachable,
|
|
3641
|
+
* the configured/static list otherwise (issue #409). Cached upstream, so the
|
|
3642
|
+
* six callers that only want one model's context window pay for at most one
|
|
3643
|
+
* fetch a minute between them.
|
|
3644
|
+
*/
|
|
3645
|
+
async availableModels() {
|
|
3646
|
+
const staticModels = this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS;
|
|
3647
|
+
return (await (0, model_catalog_1.fetchModelCatalog)(staticModels)) ?? staticModels;
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Context window for a model id. Resolving this through the live catalog is
|
|
3651
|
+
* what keeps `/session`'s "context used" percentage and `/compact`'s window
|
|
3652
|
+
* honest for a model that exists only upstream — before, any such model fell
|
|
3653
|
+
* through to the 200k default no matter how large its real window.
|
|
3654
|
+
*/
|
|
3655
|
+
async contextWindowFor(modelId) {
|
|
3656
|
+
const models = await this.availableModels();
|
|
3657
|
+
// Configured list second, not just as the catalog's fallback: the catalog
|
|
3658
|
+
// *replaces* the list rather than merging with it, so a configured model
|
|
3659
|
+
// the upstream catalog does not list (the local `[1m]` ids are exactly
|
|
3660
|
+
// that shape) would otherwise drop to the 200k default — reporting
|
|
3661
|
+
// /session context use against a fifth of the real window and handing
|
|
3662
|
+
// SessionCompactor a window small enough to compact far too early.
|
|
3663
|
+
return models.find((m) => m.id === modelId)?.contextWindow
|
|
3664
|
+
?? (this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS).find((m) => m.id === modelId)?.contextWindow
|
|
3665
|
+
?? model_catalog_1.DEFAULT_CONTEXT_WINDOW;
|
|
3666
|
+
}
|
|
3667
|
+
/**
|
|
3668
|
+
* Models `/model <arg>` may switch to: what the catalog offers, plus the
|
|
3669
|
+
* configured list.
|
|
3670
|
+
*
|
|
3671
|
+
* The union is only for *resolution*, never for the `/models` listing — the
|
|
3672
|
+
* listing has to reflect what upstream actually offers, or a model removed
|
|
3673
|
+
* upstream would linger in the picker forever, which is the bug this all
|
|
3674
|
+
* started from. Without the union a user who switches to a live-only model
|
|
3675
|
+
* is trapped: their previous configured model is no longer nameable.
|
|
3676
|
+
*/
|
|
3677
|
+
async selectableModels() {
|
|
3678
|
+
const live = await this.availableModels();
|
|
3679
|
+
const configured = this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS;
|
|
3680
|
+
const ids = new Set(live.map((m) => m.id));
|
|
3681
|
+
return [...live, ...configured.filter((m) => !ids.has(m.id))];
|
|
3682
|
+
}
|
|
3510
3683
|
async setModel(newModel) {
|
|
3511
3684
|
// Allow any model string through — BYOK/third-party models (openrouter/* etc.)
|
|
3512
3685
|
// are validated by the upstream provider, not the local config list.
|
|
@@ -3555,9 +3728,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3555
3728
|
if (!meta)
|
|
3556
3729
|
return null;
|
|
3557
3730
|
const effectiveModel = this.agentConfig.claude.model;
|
|
3558
|
-
const
|
|
3559
|
-
const modelConfig = availableModels.find((m) => m.id === effectiveModel);
|
|
3560
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
3731
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
3561
3732
|
const contextUsedPct = Math.round(((meta.lastInputTokens ?? 0) / contextWindow) * 100);
|
|
3562
3733
|
return {
|
|
3563
3734
|
sessionId: meta.id,
|