@0xmaxma/claude-gateway 1.7.9 → 1.8.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/README.md +178 -44
- package/dist/agent/builtin-commands.js +2 -2
- package/dist/agent/builtin-commands.js.map +1 -1
- package/dist/agent/dreaming/accept.d.ts.map +1 -1
- package/dist/agent/dreaming/accept.js +4 -1
- package/dist/agent/dreaming/accept.js.map +1 -1
- package/dist/agent/dreaming/config.d.ts.map +1 -1
- package/dist/agent/dreaming/config.js +2 -0
- package/dist/agent/dreaming/config.js.map +1 -1
- package/dist/agent/dreaming/reviewer.d.ts.map +1 -1
- package/dist/agent/dreaming/reviewer.js +9 -2
- package/dist/agent/dreaming/reviewer.js.map +1 -1
- package/dist/agent/dreaming/staleness.d.ts +7 -1
- package/dist/agent/dreaming/staleness.d.ts.map +1 -1
- package/dist/agent/dreaming/staleness.js +17 -2
- package/dist/agent/dreaming/staleness.js.map +1 -1
- package/dist/agent/dreaming/types.d.ts +17 -1
- package/dist/agent/dreaming/types.d.ts.map +1 -1
- package/dist/agent/knowledge/archive-db.d.ts +43 -0
- package/dist/agent/knowledge/archive-db.d.ts.map +1 -1
- package/dist/agent/knowledge/archive-db.js +130 -0
- package/dist/agent/knowledge/archive-db.js.map +1 -1
- package/dist/agent/knowledge/config.d.ts +3 -1
- package/dist/agent/knowledge/config.d.ts.map +1 -1
- package/dist/agent/knowledge/config.js +32 -10
- package/dist/agent/knowledge/config.js.map +1 -1
- package/dist/agent/knowledge/index.d.ts +11 -4
- package/dist/agent/knowledge/index.d.ts.map +1 -1
- package/dist/agent/knowledge/index.js +22 -1
- package/dist/agent/knowledge/index.js.map +1 -1
- package/dist/agent/knowledge/indexer.d.ts +0 -7
- package/dist/agent/knowledge/indexer.d.ts.map +1 -1
- package/dist/agent/knowledge/indexer.js +34 -7
- package/dist/agent/knowledge/indexer.js.map +1 -1
- package/dist/agent/knowledge/lifecycle.d.ts +20 -4
- package/dist/agent/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/agent/knowledge/lifecycle.js +33 -4
- package/dist/agent/knowledge/lifecycle.js.map +1 -1
- package/dist/agent/knowledge/reflection.d.ts +114 -0
- package/dist/agent/knowledge/reflection.d.ts.map +1 -0
- package/dist/agent/knowledge/reflection.js +447 -0
- package/dist/agent/knowledge/reflection.js.map +1 -0
- package/dist/agent/knowledge/shared-dedup.d.ts +85 -0
- package/dist/agent/knowledge/shared-dedup.d.ts.map +1 -0
- package/dist/agent/knowledge/shared-dedup.js +187 -0
- package/dist/agent/knowledge/shared-dedup.js.map +1 -0
- package/dist/agent/knowledge/shared-promote.d.ts +82 -7
- package/dist/agent/knowledge/shared-promote.d.ts.map +1 -1
- package/dist/agent/knowledge/shared-promote.js +244 -11
- package/dist/agent/knowledge/shared-promote.js.map +1 -1
- package/dist/agent/knowledge/shared-staleness.d.ts +51 -0
- package/dist/agent/knowledge/shared-staleness.d.ts.map +1 -0
- package/dist/agent/knowledge/shared-staleness.js +149 -0
- package/dist/agent/knowledge/shared-staleness.js.map +1 -0
- package/dist/agent/knowledge/shared-writer.d.ts +19 -0
- package/dist/agent/knowledge/shared-writer.d.ts.map +1 -1
- package/dist/agent/knowledge/shared-writer.js +42 -0
- package/dist/agent/knowledge/shared-writer.js.map +1 -1
- package/dist/agent/knowledge/types.d.ts +24 -0
- package/dist/agent/knowledge/types.d.ts.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 +264 -0
- package/dist/agent/model-catalog.js.map +1 -0
- package/dist/agent/runner.d.ts +83 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +281 -51
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/workspace-loader.d.ts +17 -2
- package/dist/agent/workspace-loader.d.ts.map +1 -1
- package/dist/agent/workspace-loader.js +17 -1
- package/dist/agent/workspace-loader.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 +16 -1
- 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/compose-generator.js +7 -4
- package/dist/apps/compose-generator.js.map +1 -1
- package/dist/apps/installer.d.ts +154 -0
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +657 -54
- 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 +197 -47
- 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 +20 -3
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +120 -5
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +146 -27
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/draft-phantom.d.ts +49 -0
- package/dist/shell/draft-phantom.d.ts.map +1 -0
- package/dist/shell/draft-phantom.js +59 -0
- package/dist/shell/draft-phantom.js.map +1 -0
- package/dist/shell/submit-diag.d.ts +67 -0
- package/dist/shell/submit-diag.d.ts.map +1 -0
- package/dist/shell/submit-diag.js +69 -0
- package/dist/shell/submit-diag.js.map +1 -0
- 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/types.d.ts +25 -0
- package/dist/types.d.ts.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/module.ts +23 -15
- package/mcp/tools/discord/receiver-server.ts +4 -0
- package/mcp/tools/memory/archive-reader.test.ts +25 -1
- package/mcp/tools/memory/archive-reader.ts +70 -12
- package/mcp/tools/memory/archive-writer.test.ts +168 -0
- package/mcp/tools/memory/archive-writer.ts +137 -0
- package/mcp/tools/memory/module.test.ts +187 -0
- package/mcp/tools/memory/module.ts +192 -6
- package/mcp/tools/telegram/module.ts +10 -2
- package/mcp/tools/telegram/receiver-server.ts +15 -6
- package/mcp/tools/telegram/typing.ts +178 -34
- 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,17 @@ 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
|
+
respond({ models: availableModels.map(m => ({ id: m.id, label: m.label })) });
|
|
695
|
+
})
|
|
696
|
+
// Without this the request would hang and the picker with it: the
|
|
697
|
+
// receiver is blocked on this response. Every other async branch in
|
|
698
|
+
// this handler answers on rejection too.
|
|
699
|
+
.catch((err) => respond({ success: false, error: String(err) }, 500));
|
|
690
700
|
return;
|
|
691
701
|
}
|
|
692
702
|
// `/cli` webview terminal viewer — create a pairing on behalf of an
|
|
@@ -735,15 +745,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
735
745
|
respond({ success: false, error: err.message });
|
|
736
746
|
return;
|
|
737
747
|
}
|
|
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);
|
|
748
|
+
const restarted = await this.restartChannelSessions();
|
|
747
749
|
respond({ success: true, model: newModel, restarted });
|
|
748
750
|
return;
|
|
749
751
|
}
|
|
@@ -788,9 +790,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
788
790
|
return respond({ success: true, text: 'No active session found.' });
|
|
789
791
|
}
|
|
790
792
|
const effectiveModel = this.agentConfig.claude.model;
|
|
791
|
-
const
|
|
792
|
-
const modelConfig = availableModels.find(m => m.id === effectiveModel);
|
|
793
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
793
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
794
794
|
const contextTokens = meta.lastInputTokens ?? 0;
|
|
795
795
|
const usedPct = Math.round((contextTokens / contextWindow) * 100);
|
|
796
796
|
let msgs;
|
|
@@ -1739,16 +1739,23 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1739
1739
|
// `session_idle` once it is truly done; headless mode only produces
|
|
1740
1740
|
// one `result` per message, so the timer pattern still applies there.
|
|
1741
1741
|
if (proc.backend !== 'pty-shell') {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
typingDoneTimer =
|
|
1745
|
-
|
|
1742
|
+
const waitingForBackgroundWork = proc.retainBackgroundWorkingState();
|
|
1743
|
+
if (!waitingForBackgroundWork) {
|
|
1744
|
+
typingDoneTimer = setTimeout(() => {
|
|
1745
|
+
this.writeTypingDone(mapKey);
|
|
1746
|
+
typingDoneTimer = null;
|
|
1747
|
+
}, TYPING_DONE_DELAY_MS);
|
|
1748
|
+
}
|
|
1746
1749
|
}
|
|
1747
1750
|
}
|
|
1748
1751
|
// PTY shell signals "truly idle" (no active turn, empty queue) with this
|
|
1749
1752
|
// event. Start the typing-done timer here so the indicator stays alive
|
|
1750
1753
|
// through multi-turn tool-call sequences and only stops when all work is done.
|
|
1751
1754
|
if (obj['type'] === 'session_idle' && proc.backend === 'pty-shell') {
|
|
1755
|
+
// A parent can become idle immediately after dispatching background
|
|
1756
|
+
// Agent/Workflow work. Keep the Telegram typing signal and processing
|
|
1757
|
+
// sentinel alive until the task notification starts its follow-up turn.
|
|
1758
|
+
const waitingForBackgroundWork = proc.retainBackgroundWorkingState();
|
|
1752
1759
|
// Gateway turn queue: on pty-shell this is the true end of the user's
|
|
1753
1760
|
// turn (the wrapper only emits it when its own `this.turn` is null, so
|
|
1754
1761
|
// it never fires between sub-turns) — flush the next queued turn.
|
|
@@ -1760,10 +1767,12 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1760
1767
|
}
|
|
1761
1768
|
// Skill-learning: pty-shell session_idle is the true end-of-user-turn.
|
|
1762
1769
|
this.skillLearning?.onTurnEnd(mapKey, actualSessionId);
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1770
|
+
if (!waitingForBackgroundWork) {
|
|
1771
|
+
typingDoneTimer = setTimeout(() => {
|
|
1772
|
+
this.writeTypingDone(mapKey);
|
|
1773
|
+
typingDoneTimer = null;
|
|
1774
|
+
}, TYPING_DONE_DELAY_MS);
|
|
1775
|
+
}
|
|
1767
1776
|
}
|
|
1768
1777
|
}
|
|
1769
1778
|
catch { /* non-JSON */ }
|
|
@@ -1798,6 +1807,11 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1798
1807
|
messageCountAtSpawn: proc.spawnContext.messageCountAtSpawn,
|
|
1799
1808
|
}, this.channelFor(mapKey)).catch(() => { });
|
|
1800
1809
|
}
|
|
1810
|
+
proc.on('backgroundWorkExpired', () => {
|
|
1811
|
+
// The child task never notified its parent within the bounded grace
|
|
1812
|
+
// window; release the Telegram typing signal instead of leaving it live.
|
|
1813
|
+
this.writeTypingDone(mapKey);
|
|
1814
|
+
});
|
|
1801
1815
|
// Tear down per-chat typing/processing state whenever the subprocess dies.
|
|
1802
1816
|
// Uses `on` (not `once`): a single SessionProcess can auto-restart its child
|
|
1803
1817
|
// multiple times over its lifetime, and each death that lacks a final
|
|
@@ -1865,7 +1879,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1865
1879
|
/**
|
|
1866
1880
|
* Dispatch a session management command to the appropriate handler.
|
|
1867
1881
|
*/
|
|
1868
|
-
async handleSessionCommand(chatId, content) {
|
|
1882
|
+
async handleSessionCommand(chatId, content, meta = {}) {
|
|
1869
1883
|
const agentId = this.agentConfig.id;
|
|
1870
1884
|
if (content.startsWith('/sessions')) {
|
|
1871
1885
|
await this.handleCommandSessions(agentId, chatId);
|
|
@@ -1887,10 +1901,78 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1887
1901
|
const name = content.replace('/rename', '').trim();
|
|
1888
1902
|
await this.handleCommandRename(agentId, chatId, name);
|
|
1889
1903
|
}
|
|
1904
|
+
else if (content.startsWith('/models')) {
|
|
1905
|
+
await this.handleCommandModels(chatId);
|
|
1906
|
+
}
|
|
1907
|
+
else if (content.startsWith('/model')) {
|
|
1908
|
+
await this.handleCommandModel(chatId, content.replace('/model', '').trim(), meta);
|
|
1909
|
+
}
|
|
1890
1910
|
else if (content.startsWith('/stop')) {
|
|
1891
1911
|
await this.handleCommandStop(chatId);
|
|
1892
1912
|
}
|
|
1893
1913
|
}
|
|
1914
|
+
/**
|
|
1915
|
+
* /models — the model picker for channels without an inline keyboard.
|
|
1916
|
+
*
|
|
1917
|
+
* Telegram never reaches this: its receiver answers `/models` itself with a
|
|
1918
|
+
* button keyboard and does not forward the message. Discord and LINE have no
|
|
1919
|
+
* picker of their own, so they get the list as text plus the command that
|
|
1920
|
+
* selects from it (issue #409).
|
|
1921
|
+
*
|
|
1922
|
+
* A channel listed in BUILTIN_COMMANDS but missing a branch here is worse
|
|
1923
|
+
* than one not listed at all — the gate swallows the message and the user
|
|
1924
|
+
* gets silence instead of the agent's reply. `/model` on Discord was in
|
|
1925
|
+
* exactly that state before this.
|
|
1926
|
+
*/
|
|
1927
|
+
async handleCommandModels(chatId) {
|
|
1928
|
+
const models = await this.availableModels();
|
|
1929
|
+
const current = this.agentConfig.claude.model;
|
|
1930
|
+
const lines = [`Current model: ${current}`, ''];
|
|
1931
|
+
for (const m of models) {
|
|
1932
|
+
const marker = m.id === current ? '✅ ' : '• ';
|
|
1933
|
+
lines.push(`${marker}${m.label} — \`${m.id}\``);
|
|
1934
|
+
}
|
|
1935
|
+
lines.push('', 'Switch with /model <id or alias>');
|
|
1936
|
+
this.writeAutoForward(chatId, lines.join('\n'));
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* /model — show the current model, or switch to one from the catalog.
|
|
1940
|
+
*
|
|
1941
|
+
* An unrecognised argument is refused rather than persisted: unlike the API
|
|
1942
|
+
* (where the caller is authenticated and the provider validates the id), a
|
|
1943
|
+
* typo in a chat message would otherwise be written into config.json and
|
|
1944
|
+
* break the agent's next spawn. Anything the catalog knows — including a
|
|
1945
|
+
* model that exists only upstream and not in the static list — is accepted.
|
|
1946
|
+
*/
|
|
1947
|
+
async handleCommandModel(chatId, arg, meta = {}) {
|
|
1948
|
+
const current = this.agentConfig.claude.model;
|
|
1949
|
+
if (!arg) {
|
|
1950
|
+
this.writeAutoForward(chatId, `Current model: ${current}\n\nUse /models to see the list.`);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (!this.isDirectChat(meta)) {
|
|
1954
|
+
this.writeAutoForward(chatId, 'Switching models is only available in a direct message with the bot — '
|
|
1955
|
+
+ 'it changes the model for every chat of this agent and restarts them. '
|
|
1956
|
+
+ 'Use /models here to see the list.');
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const models = await this.selectableModels();
|
|
1960
|
+
const wanted = arg.toLowerCase();
|
|
1961
|
+
const match = models.find((m) => m.id.toLowerCase() === wanted || m.alias.toLowerCase() === wanted);
|
|
1962
|
+
if (!match) {
|
|
1963
|
+
this.writeAutoForward(chatId, `Unknown model "${arg}". Use /models to see the list.`);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
if (match.id === current) {
|
|
1967
|
+
this.writeAutoForward(chatId, `Already using ${match.label} (${match.id}).`);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
await this.setModel(match.id);
|
|
1971
|
+
// Same restart the Telegram picker's set_model does — the running session
|
|
1972
|
+
// was spawned with the old model and would otherwise keep using it.
|
|
1973
|
+
const restarted = await this.restartChannelSessions();
|
|
1974
|
+
this.writeAutoForward(chatId, `Model set to ${match.label} (${match.id}).${restarted ? ' Restarting the session…' : ''}`);
|
|
1975
|
+
}
|
|
1894
1976
|
/**
|
|
1895
1977
|
* /sessions — list all sessions for this chat.
|
|
1896
1978
|
*/
|
|
@@ -1919,9 +2001,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
1919
2001
|
return;
|
|
1920
2002
|
}
|
|
1921
2003
|
const effectiveModel = this.agentConfig.claude.model;
|
|
1922
|
-
const
|
|
1923
|
-
const modelConfig = availableModels.find(m => m.id === effectiveModel);
|
|
1924
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
2004
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
1925
2005
|
const contextTokens = meta.lastInputTokens ?? 0;
|
|
1926
2006
|
const usedPct = Math.round((contextTokens / contextWindow) * 100);
|
|
1927
2007
|
let msgs;
|
|
@@ -2039,9 +2119,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2039
2119
|
const meta = index.sessions.find(s => s.id === sessionId);
|
|
2040
2120
|
const name = meta?.name ?? 'Session';
|
|
2041
2121
|
const compactModel = this.agentConfig.claude.model;
|
|
2042
|
-
const
|
|
2043
|
-
const modelConfig = availableModels.find(m => m.id === compactModel);
|
|
2044
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
2122
|
+
const contextWindow = await this.contextWindowFor(compactModel);
|
|
2045
2123
|
this.writeAutoForward(chatId, `⏳ Compacting session "${name}"...`);
|
|
2046
2124
|
try {
|
|
2047
2125
|
const compactor = new compactor_1.SessionCompactor(this.sessionStore);
|
|
@@ -2239,12 +2317,25 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2239
2317
|
proc.markPendingRestart();
|
|
2240
2318
|
deferred++;
|
|
2241
2319
|
}
|
|
2242
|
-
else if (deferIdle && proc.source !== 'api') {
|
|
2243
|
-
// Lossless deferral for CHANNEL sessions
|
|
2244
|
-
//
|
|
2245
|
-
//
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2320
|
+
else if ((proc.hasLikelyOutstandingBackgroundWork() || deferIdle) && proc.source !== 'api') {
|
|
2321
|
+
// Lossless deferral for CHANNEL sessions: leave the idle process running
|
|
2322
|
+
// and arm a restart on its next message instead of an immediate stop.
|
|
2323
|
+
// Two independent reasons land here, neither gated by skipBusy (which
|
|
2324
|
+
// only governs the isProcessing branch above — a session that's already
|
|
2325
|
+
// idle isn't the self-restart-footgun case skipBusy exists for):
|
|
2326
|
+
// - deferIdle: the caller explicitly asked never to SIGKILL an idle
|
|
2327
|
+
// bystander (mirrors the image-size-threshold path).
|
|
2328
|
+
// - hasLikelyOutstandingBackgroundWork(): regardless of which tier
|
|
2329
|
+
// called restartOrDefer — even today's aggressive default — a
|
|
2330
|
+
// session that dispatched a background Agent/Workflow tool_use and
|
|
2331
|
+
// then ended its OWN turn looks idle here, but a sub-agent it
|
|
2332
|
+
// launched may still be doing real work. SIGKILLing it now would
|
|
2333
|
+
// silently destroy that in-flight work with no notification or
|
|
2334
|
+
// recovery (the incident this guards against: a CLAUDE.md-triggered
|
|
2335
|
+
// restart killed a session mid-way through 3 dispatched code-review
|
|
2336
|
+
// sub-agents).
|
|
2337
|
+
// Do NOT call proc.markPendingRestart() here — on an idle process it
|
|
2338
|
+
// fires deferredRestartReady immediately and stops the session.
|
|
2248
2339
|
//
|
|
2249
2340
|
// Guard on source: `pendingRestarts` is consumed ONLY in injectTurn (the
|
|
2250
2341
|
// channel turn path, keyed by chatId). api / __heartbeat__ sessions are
|
|
@@ -2296,6 +2387,22 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2296
2387
|
const stateDir = channel === 'discord' ? '.discord-state' : '.telegram-state';
|
|
2297
2388
|
return path.join(this.agentConfig.workspace, stateDir, 'typing');
|
|
2298
2389
|
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Read the live typing-signal file's timestamp as this turn's id. Shared
|
|
2392
|
+
* with the `telegram_reply` MCP tool (a separate process) purely via this
|
|
2393
|
+
* file, so `.forward`/`.replied` markers written around the same turn carry
|
|
2394
|
+
* a matching id — see writeAutoForward()'s dedup comment.
|
|
2395
|
+
*/
|
|
2396
|
+
readCurrentTurnId(chatId) {
|
|
2397
|
+
const typingDir = this.getTypingDir(chatId);
|
|
2398
|
+
try {
|
|
2399
|
+
const raw = fs.readFileSync(path.join(typingDir, chatId), 'utf8').trim();
|
|
2400
|
+
return raw || null;
|
|
2401
|
+
}
|
|
2402
|
+
catch {
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2299
2406
|
writeTypingError(chatId, code) {
|
|
2300
2407
|
const typingDir = this.getTypingDir(chatId);
|
|
2301
2408
|
try {
|
|
@@ -2365,9 +2472,39 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2365
2472
|
return;
|
|
2366
2473
|
}
|
|
2367
2474
|
const typingDir = this.getTypingDir(chatId);
|
|
2475
|
+
const forwardPath = path.join(typingDir, `${chatId}.forward`);
|
|
2368
2476
|
try {
|
|
2369
2477
|
fs.mkdirSync(typingDir, { recursive: true });
|
|
2370
|
-
|
|
2478
|
+
// Append rather than overwrite: two forward-worthy events for the same
|
|
2479
|
+
// chat before the typing loop drains (e.g. two command replies, or the
|
|
2480
|
+
// race where a queued turn's stop() call drains a prior turn's leftover
|
|
2481
|
+
// state) must both reach the user, not have the second clobber the
|
|
2482
|
+
// first. turnId is the live typing-signal timestamp — the receiver uses
|
|
2483
|
+
// it to scope the `.replied` dedup marker to the turn that wrote it.
|
|
2484
|
+
let entries = [];
|
|
2485
|
+
try {
|
|
2486
|
+
const raw = fs.readFileSync(forwardPath, 'utf8').trim();
|
|
2487
|
+
if (raw) {
|
|
2488
|
+
try {
|
|
2489
|
+
const parsed = JSON.parse(raw);
|
|
2490
|
+
entries = Array.isArray(parsed)
|
|
2491
|
+
? parsed
|
|
2492
|
+
: [parsed];
|
|
2493
|
+
}
|
|
2494
|
+
catch {
|
|
2495
|
+
entries = [{ text: raw, format: 'text', turnId: null }];
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
catch {
|
|
2500
|
+
// No existing file — fresh queue.
|
|
2501
|
+
}
|
|
2502
|
+
entries.push({ text, format, turnId: this.readCurrentTurnId(chatId) });
|
|
2503
|
+
// Atomic write (tmp + rename): the receiver polls this directory on
|
|
2504
|
+
// every typing tick, so a non-atomic write could be read mid-flush.
|
|
2505
|
+
const tmpPath = `${forwardPath}.tmp`;
|
|
2506
|
+
fs.writeFileSync(tmpPath, JSON.stringify(entries));
|
|
2507
|
+
fs.renameSync(tmpPath, forwardPath);
|
|
2371
2508
|
}
|
|
2372
2509
|
catch {
|
|
2373
2510
|
// Non-fatal
|
|
@@ -2397,6 +2534,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2397
2534
|
startIdleCleaner() {
|
|
2398
2535
|
this.idleCleanerTimer = setInterval(async () => {
|
|
2399
2536
|
for (const [id, proc] of this.sessions) {
|
|
2537
|
+
// A parent that just dispatched Agent/Workflow work is CLI-idle but its
|
|
2538
|
+
// sub-agent may still need this session to receive the task notification.
|
|
2539
|
+
if (proc.hasLikelyOutstandingBackgroundWork())
|
|
2540
|
+
continue;
|
|
2400
2541
|
if (proc.isIdle(this.idleTimeoutMs)) {
|
|
2401
2542
|
this.logger.info('Stopping idle session', { sessionId: id });
|
|
2402
2543
|
await proc.stop();
|
|
@@ -2494,7 +2635,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2494
2635
|
stopTelegramReceiver() {
|
|
2495
2636
|
if (!this.receiver)
|
|
2496
2637
|
return;
|
|
2497
|
-
|
|
2638
|
+
// Hot-remove is a config-change response, not a shutdown: detach immediately
|
|
2639
|
+
// and let teardown finish in the background rather than blocking the caller
|
|
2640
|
+
// for the SIGKILL grace period. stop() never rejects.
|
|
2641
|
+
void this.receiver.stop();
|
|
2498
2642
|
this.receiver = null;
|
|
2499
2643
|
this.logger.info('TelegramReceiver stopped', { agentId: this.agentConfig.id });
|
|
2500
2644
|
}
|
|
@@ -2510,7 +2654,10 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2510
2654
|
stopDiscordReceiver() {
|
|
2511
2655
|
if (!this.discordReceiver)
|
|
2512
2656
|
return;
|
|
2513
|
-
|
|
2657
|
+
// Hot-remove is a config-change response, not a shutdown: detach immediately
|
|
2658
|
+
// and let teardown finish in the background rather than blocking the caller
|
|
2659
|
+
// for the SIGKILL grace period. stop() never rejects.
|
|
2660
|
+
void this.discordReceiver.stop();
|
|
2514
2661
|
this.discordReceiver = null;
|
|
2515
2662
|
this.logger.info('DiscordReceiver stopped', { agentId: this.agentConfig.id });
|
|
2516
2663
|
}
|
|
@@ -2532,10 +2679,17 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
2532
2679
|
clearTimeout(buf.timer);
|
|
2533
2680
|
}
|
|
2534
2681
|
this.channelCoalesce.clear();
|
|
2535
|
-
|
|
2536
|
-
|
|
2682
|
+
// Receiver teardown now resolves only once the child has exited, so it must
|
|
2683
|
+
// be awaited or shutdown() can exit the gateway out from under a receiver
|
|
2684
|
+
// that is still shutting down (issue #405). Run it concurrently with session
|
|
2685
|
+
// teardown rather than serially — both are bounded, neither depends on the
|
|
2686
|
+
// other, and shutdown latency is user-visible.
|
|
2687
|
+
const receiversStopped = [this.receiver?.stop(), this.discordReceiver?.stop()];
|
|
2537
2688
|
this.stopLineReply();
|
|
2538
|
-
await Promise.all([
|
|
2689
|
+
await Promise.all([
|
|
2690
|
+
...receiversStopped,
|
|
2691
|
+
...[...this.sessions.values()].map((s) => s.stop()),
|
|
2692
|
+
]);
|
|
2539
2693
|
this.sessions.clear();
|
|
2540
2694
|
}
|
|
2541
2695
|
/**
|
|
@@ -3335,9 +3489,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3335
3489
|
responseText = `Session: (unnamed)\nMessages: ${messageCount}\nContext used: 0%\nModel: ${effectiveModel}`;
|
|
3336
3490
|
}
|
|
3337
3491
|
else {
|
|
3338
|
-
const
|
|
3339
|
-
const modelConfig = availableModels.find((m) => m.id === effectiveModel);
|
|
3340
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
3492
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
3341
3493
|
const contextUsedPct = Math.round(((meta.lastInputTokens ?? 0) / contextWindow) * 100);
|
|
3342
3494
|
result = { sessionId, sessionName: meta.name, messageCount, archivedCount: meta.archivedCount ?? 0, contextUsedPct, model: effectiveModel };
|
|
3343
3495
|
responseText = [
|
|
@@ -3394,9 +3546,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3394
3546
|
const ch = 'api';
|
|
3395
3547
|
const activeSession = this.sessions.get(sessionId);
|
|
3396
3548
|
const compactEffectiveModel = opts?.model ?? activeSession?.modelOverride ?? this.agentConfig.claude.model;
|
|
3397
|
-
const
|
|
3398
|
-
const modelConfig = availableModels.find((m) => m.id === compactEffectiveModel);
|
|
3399
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
3549
|
+
const contextWindow = await this.contextWindowFor(compactEffectiveModel);
|
|
3400
3550
|
const compactor = new compactor_1.SessionCompactor(this.sessionStore);
|
|
3401
3551
|
const compactResult = await compactor.compact(agentId, storeChatId, sessionId, compactEffectiveModel, contextWindow, ch);
|
|
3402
3552
|
await this.sessionStore.updateSessionMeta(agentId, storeChatId, sessionId, {
|
|
@@ -3430,6 +3580,88 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3430
3580
|
persist('assistant', responseText, forcePersist);
|
|
3431
3581
|
return { result, responseText };
|
|
3432
3582
|
}
|
|
3583
|
+
/**
|
|
3584
|
+
* Whether an inbound channel message came from a 1:1 chat with the bot.
|
|
3585
|
+
*
|
|
3586
|
+
* Gates the one channel command that reaches past its own chat: `/model <x>`
|
|
3587
|
+
* rewrites `config.json` for the whole agent and restarts every session in
|
|
3588
|
+
* every chat. The channel access gates do not cover that. Discord's, in a
|
|
3589
|
+
* guild, checks the guild, the channel, and a mention — never the user
|
|
3590
|
+
* (`allowFrom` in mcp/tools/discord/access.ts is DM-only), so without this
|
|
3591
|
+
* any member of an allowlisted channel could switch the model out from under
|
|
3592
|
+
* everyone else and kill their in-flight turns. LINE forwards group and room
|
|
3593
|
+
* messages the same way. Telegram's receiver answers `/model` itself behind
|
|
3594
|
+
* a private-chat + allowlist check and does not forward it here.
|
|
3595
|
+
*
|
|
3596
|
+
* Fails closed: a channel that does not report a chat type cannot switch.
|
|
3597
|
+
* Listing models (`/models`, bare `/model`) is unaffected — it reads nothing
|
|
3598
|
+
* and changes nothing.
|
|
3599
|
+
*/
|
|
3600
|
+
isDirectChat(meta) {
|
|
3601
|
+
const kind = meta['chat_type'] ?? meta['line_chat_type'] ?? meta['slack_chat_type'];
|
|
3602
|
+
return kind === 'direct' || kind === 'user';
|
|
3603
|
+
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Restart every non-api session so a model change actually takes effect.
|
|
3606
|
+
*
|
|
3607
|
+
* `setModel` only rewrites config — a session process was spawned with the
|
|
3608
|
+
* old model on its command line and keeps using it until it is restarted.
|
|
3609
|
+
* Reporting "model set" without this is a false success: the next turn still
|
|
3610
|
+
* runs on the previous model. Returns whether anything was restarted.
|
|
3611
|
+
*/
|
|
3612
|
+
async restartChannelSessions() {
|
|
3613
|
+
const restartPromises = [];
|
|
3614
|
+
for (const [key, session] of this.sessions) {
|
|
3615
|
+
if (session.source !== 'api')
|
|
3616
|
+
restartPromises.push(this.restartProcess(key));
|
|
3617
|
+
}
|
|
3618
|
+
await Promise.all(restartPromises);
|
|
3619
|
+
return restartPromises.length > 0;
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* The model list this gateway offers: the live catalog when one is reachable,
|
|
3623
|
+
* the configured/static list otherwise (issue #409). Cached upstream, so the
|
|
3624
|
+
* six callers that only want one model's context window pay for at most one
|
|
3625
|
+
* fetch a minute between them.
|
|
3626
|
+
*/
|
|
3627
|
+
async availableModels() {
|
|
3628
|
+
const staticModels = this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS;
|
|
3629
|
+
return (await (0, model_catalog_1.fetchModelCatalog)(staticModels)) ?? staticModels;
|
|
3630
|
+
}
|
|
3631
|
+
/**
|
|
3632
|
+
* Context window for a model id. Resolving this through the live catalog is
|
|
3633
|
+
* what keeps `/session`'s "context used" percentage and `/compact`'s window
|
|
3634
|
+
* honest for a model that exists only upstream — before, any such model fell
|
|
3635
|
+
* through to the 200k default no matter how large its real window.
|
|
3636
|
+
*/
|
|
3637
|
+
async contextWindowFor(modelId) {
|
|
3638
|
+
const models = await this.availableModels();
|
|
3639
|
+
// Configured list second, not just as the catalog's fallback: the catalog
|
|
3640
|
+
// *replaces* the list rather than merging with it, so a configured model
|
|
3641
|
+
// the upstream catalog does not list (the local `[1m]` ids are exactly
|
|
3642
|
+
// that shape) would otherwise drop to the 200k default — reporting
|
|
3643
|
+
// /session context use against a fifth of the real window and handing
|
|
3644
|
+
// SessionCompactor a window small enough to compact far too early.
|
|
3645
|
+
return models.find((m) => m.id === modelId)?.contextWindow
|
|
3646
|
+
?? (this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS).find((m) => m.id === modelId)?.contextWindow
|
|
3647
|
+
?? model_catalog_1.DEFAULT_CONTEXT_WINDOW;
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Models `/model <arg>` may switch to: what the catalog offers, plus the
|
|
3651
|
+
* configured list.
|
|
3652
|
+
*
|
|
3653
|
+
* The union is only for *resolution*, never for the `/models` listing — the
|
|
3654
|
+
* listing has to reflect what upstream actually offers, or a model removed
|
|
3655
|
+
* upstream would linger in the picker forever, which is the bug this all
|
|
3656
|
+
* started from. Without the union a user who switches to a live-only model
|
|
3657
|
+
* is trapped: their previous configured model is no longer nameable.
|
|
3658
|
+
*/
|
|
3659
|
+
async selectableModels() {
|
|
3660
|
+
const live = await this.availableModels();
|
|
3661
|
+
const configured = this.gatewayConfig.gateway.models ?? exports.DEFAULT_MODELS;
|
|
3662
|
+
const ids = new Set(live.map((m) => m.id));
|
|
3663
|
+
return [...live, ...configured.filter((m) => !ids.has(m.id))];
|
|
3664
|
+
}
|
|
3433
3665
|
async setModel(newModel) {
|
|
3434
3666
|
// Allow any model string through — BYOK/third-party models (openrouter/* etc.)
|
|
3435
3667
|
// are validated by the upstream provider, not the local config list.
|
|
@@ -3478,9 +3710,7 @@ class AgentRunner extends events_1.EventEmitter {
|
|
|
3478
3710
|
if (!meta)
|
|
3479
3711
|
return null;
|
|
3480
3712
|
const effectiveModel = this.agentConfig.claude.model;
|
|
3481
|
-
const
|
|
3482
|
-
const modelConfig = availableModels.find((m) => m.id === effectiveModel);
|
|
3483
|
-
const contextWindow = modelConfig?.contextWindow ?? 200000;
|
|
3713
|
+
const contextWindow = await this.contextWindowFor(effectiveModel);
|
|
3484
3714
|
const contextUsedPct = Math.round(((meta.lastInputTokens ?? 0) / contextWindow) * 100);
|
|
3485
3715
|
return {
|
|
3486
3716
|
sessionId: meta.id,
|