@0xmaxma/claude-gateway 1.0.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 +791 -0
- package/config.template.json +87 -0
- package/dist/agent/context-isolation.d.ts +28 -0
- package/dist/agent/context-isolation.d.ts.map +1 -0
- package/dist/agent/context-isolation.js +80 -0
- package/dist/agent/context-isolation.js.map +1 -0
- package/dist/agent/runner.d.ts +211 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +1802 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/workspace-loader.d.ts +31 -0
- package/dist/agent/workspace-loader.d.ts.map +1 -0
- package/dist/agent/workspace-loader.js +207 -0
- package/dist/agent/workspace-loader.js.map +1 -0
- package/dist/api/auth.d.ts +25 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +77 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/cron-router.d.ts +22 -0
- package/dist/api/cron-router.d.ts.map +1 -0
- package/dist/api/cron-router.js +197 -0
- package/dist/api/cron-router.js.map +1 -0
- package/dist/api/gateway-router.d.ts +52 -0
- package/dist/api/gateway-router.d.ts.map +1 -0
- package/dist/api/gateway-router.js +215 -0
- package/dist/api/gateway-router.js.map +1 -0
- package/dist/api/router.d.ts +5 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +1098 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/skills-router.d.ts +5 -0
- package/dist/api/skills-router.d.ts.map +1 -0
- package/dist/api/skills-router.js +424 -0
- package/dist/api/skills-router.js.map +1 -0
- package/dist/api/workspace-router.d.ts +4 -0
- package/dist/api/workspace-router.d.ts.map +1 -0
- package/dist/api/workspace-router.js +149 -0
- package/dist/api/workspace-router.js.map +1 -0
- package/dist/config/loader.d.ts +16 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +235 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/migrator.d.ts +70 -0
- package/dist/config/migrator.d.ts.map +1 -0
- package/dist/config/migrator.js +343 -0
- package/dist/config/migrator.js.map +1 -0
- package/dist/config/watcher.d.ts +30 -0
- package/dist/config/watcher.d.ts.map +1 -0
- package/dist/config/watcher.js +170 -0
- package/dist/config/watcher.js.map +1 -0
- package/dist/cron/manager.d.ts +48 -0
- package/dist/cron/manager.d.ts.map +1 -0
- package/dist/cron/manager.js +580 -0
- package/dist/cron/manager.js.map +1 -0
- package/dist/cron/scheduler.d.ts +50 -0
- package/dist/cron/scheduler.d.ts.map +1 -0
- package/dist/cron/scheduler.js +231 -0
- package/dist/cron/scheduler.js.map +1 -0
- package/dist/discord/receiver.d.ts +17 -0
- package/dist/discord/receiver.d.ts.map +1 -0
- package/dist/discord/receiver.js +108 -0
- package/dist/discord/receiver.js.map +1 -0
- package/dist/heartbeat/history.d.ts +24 -0
- package/dist/heartbeat/history.d.ts.map +1 -0
- package/dist/heartbeat/history.js +50 -0
- package/dist/heartbeat/history.js.map +1 -0
- package/dist/heartbeat/parser.d.ts +26 -0
- package/dist/heartbeat/parser.d.ts.map +1 -0
- package/dist/heartbeat/parser.js +283 -0
- package/dist/heartbeat/parser.js.map +1 -0
- package/dist/history/cleanup.d.ts +21 -0
- package/dist/history/cleanup.d.ts.map +1 -0
- package/dist/history/cleanup.js +171 -0
- package/dist/history/cleanup.js.map +1 -0
- package/dist/history/db.d.ts +27 -0
- package/dist/history/db.d.ts.map +1 -0
- package/dist/history/db.js +323 -0
- package/dist/history/db.js.map +1 -0
- package/dist/history/media-store.d.ts +27 -0
- package/dist/history/media-store.d.ts.map +1 -0
- package/dist/history/media-store.js +158 -0
- package/dist/history/media-store.js.map +1 -0
- package/dist/history/types.d.ts +62 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +3 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/manager.d.ts +45 -0
- package/dist/memory/manager.d.ts.map +1 -0
- package/dist/memory/manager.js +178 -0
- package/dist/memory/manager.js.map +1 -0
- package/dist/session/compactor.d.ts +22 -0
- package/dist/session/compactor.d.ts.map +1 -0
- package/dist/session/compactor.js +151 -0
- package/dist/session/compactor.js.map +1 -0
- package/dist/session/process.d.ts +76 -0
- package/dist/session/process.d.ts.map +1 -0
- package/dist/session/process.js +729 -0
- package/dist/session/process.js.map +1 -0
- package/dist/session/store.d.ts +102 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +533 -0
- package/dist/session/store.js.map +1 -0
- package/dist/skills/index.d.ts +9 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +15 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/invoker.d.ts +16 -0
- package/dist/skills/invoker.d.ts.map +1 -0
- package/dist/skills/invoker.js +36 -0
- package/dist/skills/invoker.js.map +1 -0
- package/dist/skills/loader.d.ts +23 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +183 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parser.d.ts +55 -0
- package/dist/skills/parser.d.ts.map +1 -0
- package/dist/skills/parser.js +178 -0
- package/dist/skills/parser.js.map +1 -0
- package/dist/skills/sync.d.ts +10 -0
- package/dist/skills/sync.d.ts.map +1 -0
- package/dist/skills/sync.js +130 -0
- package/dist/skills/sync.js.map +1 -0
- package/dist/skills/watcher.d.ts +16 -0
- package/dist/skills/watcher.d.ts.map +1 -0
- package/dist/skills/watcher.js +25 -0
- package/dist/skills/watcher.js.map +1 -0
- package/dist/telegram/markdown.d.ts +25 -0
- package/dist/telegram/markdown.d.ts.map +1 -0
- package/dist/telegram/markdown.js +237 -0
- package/dist/telegram/markdown.js.map +1 -0
- package/dist/telegram/receiver.d.ts +17 -0
- package/dist/telegram/receiver.d.ts.map +1 -0
- package/dist/telegram/receiver.js +104 -0
- package/dist/telegram/receiver.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/web-ui.d.ts +6 -0
- package/dist/ui/web-ui.d.ts.map +1 -0
- package/dist/ui/web-ui.js +227 -0
- package/dist/ui/web-ui.js.map +1 -0
- package/dist/watch/factory.d.ts +26 -0
- package/dist/watch/factory.d.ts.map +1 -0
- package/dist/watch/factory.js +43 -0
- package/dist/watch/factory.js.map +1 -0
- package/dist/webhook/manager.d.ts +34 -0
- package/dist/webhook/manager.d.ts.map +1 -0
- package/dist/webhook/manager.js +120 -0
- package/dist/webhook/manager.js.map +1 -0
- package/mcp/bun.lock +264 -0
- package/mcp/channel-manager.ts +115 -0
- package/mcp/package.json +15 -0
- package/mcp/router.ts +69 -0
- package/mcp/server.ts +129 -0
- package/mcp/tools/agent/handlers.ts +578 -0
- package/mcp/tools/agent/module.ts +163 -0
- package/mcp/tools/browser/module.ts +321 -0
- package/mcp/tools/browser/skills/open-browser/SKILL.md +89 -0
- package/mcp/tools/cron/client.ts +65 -0
- package/mcp/tools/cron/module.ts +158 -0
- package/mcp/tools/cron/skills/cron/SKILL.md +46 -0
- package/mcp/tools/discord/access.ts +197 -0
- package/mcp/tools/discord/client.ts +31 -0
- package/mcp/tools/discord/commands.ts +48 -0
- package/mcp/tools/discord/inbound.ts +53 -0
- package/mcp/tools/discord/module.ts +442 -0
- package/mcp/tools/discord/outbound.ts +90 -0
- package/mcp/tools/discord/receiver-server.ts +58 -0
- package/mcp/tools/discord/skills/access/SKILL.md +86 -0
- package/mcp/tools/discord/skills/configure/SKILL.md +53 -0
- package/mcp/tools/discord/threading.ts +37 -0
- package/mcp/tools/discord/types.ts +106 -0
- package/mcp/tools/skills/handlers.ts +196 -0
- package/mcp/tools/skills/module.ts +163 -0
- package/mcp/tools/telegram/module.ts +454 -0
- package/mcp/tools/telegram/pure.ts +224 -0
- package/mcp/tools/telegram/receiver-server.ts +1656 -0
- package/mcp/tools/telegram/skills/access/SKILL.md +169 -0
- package/mcp/tools/telegram/skills/configure/SKILL.md +118 -0
- package/mcp/tools/telegram/typing.ts +327 -0
- package/mcp/types.ts +128 -0
- package/package.json +62 -0
- package/resource/claude_gateway.svg +112 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SessionProcess = void 0;
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const events_1 = require("events");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
46
|
+
const logger_1 = require("../logger");
|
|
47
|
+
const MAX_HISTORY_MESSAGES = 50;
|
|
48
|
+
const AUTO_RESTART_DELAY_MS = 5000;
|
|
49
|
+
const MAX_RESTARTS = 3;
|
|
50
|
+
const CHANNELS_ACTIVATION_PROMPT = 'Channels mode is active. Wait for incoming messages from your channels and respond to them.';
|
|
51
|
+
class SessionProcess extends events_1.EventEmitter {
|
|
52
|
+
constructor(sessionId, source, agentConfig, gatewayConfig, sessionStore, chatId) {
|
|
53
|
+
super();
|
|
54
|
+
this.lastActivityAt = Date.now(); // accessible by AgentRunner for eviction sort
|
|
55
|
+
this.spawnContext = null;
|
|
56
|
+
this.process = null;
|
|
57
|
+
this.stopping = false;
|
|
58
|
+
this.restartCount = 0;
|
|
59
|
+
this.restartRequested = false;
|
|
60
|
+
this._processing = false;
|
|
61
|
+
this._pendingRestart = false;
|
|
62
|
+
this.restartWatcher = null;
|
|
63
|
+
this.queryMode = false;
|
|
64
|
+
this._queryBuffer = '';
|
|
65
|
+
this._querySettled = false;
|
|
66
|
+
this.sessionId = sessionId;
|
|
67
|
+
this.source = source;
|
|
68
|
+
this.chatId = chatId ?? sessionId;
|
|
69
|
+
this.sessionChannel = source === 'discord' ? 'discord' : 'telegram';
|
|
70
|
+
this.agentConfig = agentConfig;
|
|
71
|
+
this.gatewayConfig = gatewayConfig;
|
|
72
|
+
this.sessionStore = sessionStore;
|
|
73
|
+
this.logger = (0, logger_1.createLogger)(`${agentConfig.id}:session:${sessionId}`, gatewayConfig.gateway.logDir);
|
|
74
|
+
// config.json lives 3 levels above workspace: <base>/<agentId>/workspace → <base>/config.json
|
|
75
|
+
this.configPath = path.resolve(agentConfig.workspace, '..', '..', '..', 'config.json');
|
|
76
|
+
const stateSubDir = source === 'discord' ? '.discord-state' : '.telegram-state';
|
|
77
|
+
this.restartSignalPath = path.join(agentConfig.workspace, stateSubDir, `restart-${sessionId}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Read model from config.json on disk so restarts pick up changes made after startup.
|
|
81
|
+
* Falls back to the cached agentConfig.claude.model if config can't be read.
|
|
82
|
+
*/
|
|
83
|
+
readFreshModel() {
|
|
84
|
+
try {
|
|
85
|
+
const raw = fs.readFileSync(this.configPath, 'utf-8');
|
|
86
|
+
const config = JSON.parse(raw);
|
|
87
|
+
const found = config.agents?.find(a => a.id === this.agentConfig.id);
|
|
88
|
+
if (found?.claude?.model)
|
|
89
|
+
return found.claude.model;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// fallback to cached value
|
|
93
|
+
}
|
|
94
|
+
return this.agentConfig.claude.model;
|
|
95
|
+
}
|
|
96
|
+
async start() {
|
|
97
|
+
this.stopping = false;
|
|
98
|
+
this.restartCount = 0;
|
|
99
|
+
this.setupRestartWatcher();
|
|
100
|
+
await this.spawnProcess();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Watch for a restart signal file written by the agent.
|
|
104
|
+
* When detected, kill the current Claude process — scheduleRestart() will
|
|
105
|
+
* re-spawn it with the latest model from config.json.
|
|
106
|
+
*/
|
|
107
|
+
setupRestartWatcher() {
|
|
108
|
+
if (this.restartWatcher)
|
|
109
|
+
return;
|
|
110
|
+
this.restartWatcher = chokidar_1.default.watch(this.restartSignalPath, { ignoreInitial: true });
|
|
111
|
+
this.restartWatcher.on('add', () => {
|
|
112
|
+
// Read signal file content before deleting — may contain a notify payload
|
|
113
|
+
let notifyPayload = null;
|
|
114
|
+
try {
|
|
115
|
+
const content = fs.readFileSync(this.restartSignalPath, 'utf-8').trim();
|
|
116
|
+
if (content) {
|
|
117
|
+
const parsed = JSON.parse(content);
|
|
118
|
+
notifyPayload = parsed.notify ?? null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch { /* empty or unparseable — no notify */ }
|
|
122
|
+
try {
|
|
123
|
+
fs.rmSync(this.restartSignalPath, { force: true });
|
|
124
|
+
}
|
|
125
|
+
catch { }
|
|
126
|
+
this.restartRequested = true;
|
|
127
|
+
this.logger.info('Graceful restart requested', { sessionId: this.sessionId, hasNotify: !!notifyPayload });
|
|
128
|
+
// Inject a marker into session history so the next spawned session
|
|
129
|
+
// knows the restart is complete and does not repeat it.
|
|
130
|
+
// If notify payload is present, include instruction for the agent to send a message after restart.
|
|
131
|
+
const marker = notifyPayload
|
|
132
|
+
? `[System: Graceful restart completed successfully. Do not restart again. IMPORTANT: Send a Telegram reply to chat_id "${notifyPayload.chat_id}" with the message: "${notifyPayload.text}"]`
|
|
133
|
+
: '[System: Graceful restart completed successfully. Do not restart again.]';
|
|
134
|
+
const restartMsg = { role: 'assistant', content: marker, ts: Date.now() };
|
|
135
|
+
const appendRestartMarker = this.source !== 'api'
|
|
136
|
+
? this.sessionStore.appendTelegramMessage(this.agentConfig.id, this.chatId, this.sessionId, restartMsg, this.sessionChannel)
|
|
137
|
+
: this.sessionStore.appendMessage(this.agentConfig.id, this.sessionId, restartMsg);
|
|
138
|
+
appendRestartMarker.catch(err => this.logger.warn('Failed to write restart marker', { error: err.message }));
|
|
139
|
+
if (this.process) {
|
|
140
|
+
this.process.kill('SIGTERM');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async buildInitialPrompt() {
|
|
145
|
+
const history = this.source !== 'api'
|
|
146
|
+
? await this.sessionStore.loadTelegramSession(this.agentConfig.id, this.chatId, this.sessionId, this.sessionChannel)
|
|
147
|
+
: await this.sessionStore.loadSession(this.agentConfig.id, this.sessionId);
|
|
148
|
+
// If history exceeds the limit and history[0] is a compaction summary, rescue it
|
|
149
|
+
// so the model retains context from before the truncation window.
|
|
150
|
+
const SUMMARY_MARKER = '[Conversation Summary]';
|
|
151
|
+
const firstMsg = history[0];
|
|
152
|
+
const hasSummary = history.length > MAX_HISTORY_MESSAGES &&
|
|
153
|
+
firstMsg?.role === 'system' &&
|
|
154
|
+
typeof firstMsg.content === 'string' &&
|
|
155
|
+
firstMsg.content.trimStart().startsWith(SUMMARY_MARKER);
|
|
156
|
+
const recent = hasSummary
|
|
157
|
+
? [firstMsg, ...history.slice(-(MAX_HISTORY_MESSAGES - 1))]
|
|
158
|
+
: history.slice(-MAX_HISTORY_MESSAGES);
|
|
159
|
+
const loadedAtSpawn = recent.length;
|
|
160
|
+
const archivedCount = history.length - recent.length;
|
|
161
|
+
const messageCountAtSpawn = history.length;
|
|
162
|
+
if (recent.length === 0) {
|
|
163
|
+
return { prompt: CHANNELS_ACTIVATION_PROMPT, loadedAtSpawn, archivedCount, messageCountAtSpawn };
|
|
164
|
+
}
|
|
165
|
+
const historyText = recent
|
|
166
|
+
.map(m => {
|
|
167
|
+
// system role carries injected summaries (e.g. [Image Context Summary]) from the runner
|
|
168
|
+
if (m.role === 'system')
|
|
169
|
+
return `System: ${m.content}`;
|
|
170
|
+
return `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`;
|
|
171
|
+
})
|
|
172
|
+
.join('\n');
|
|
173
|
+
return {
|
|
174
|
+
prompt: `[Conversation history with this user:\n${historyText}]\n\n${CHANNELS_ACTIVATION_PROMPT}`,
|
|
175
|
+
loadedAtSpawn,
|
|
176
|
+
archivedCount,
|
|
177
|
+
messageCountAtSpawn,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Read stdio MCP servers from Claude Code's user-scoped config (~/.claude/settings.json).
|
|
182
|
+
* Returns empty object if file doesn't exist, can't be parsed, or has no mcpServers.
|
|
183
|
+
*/
|
|
184
|
+
readUserScopedMcp() {
|
|
185
|
+
try {
|
|
186
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
187
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
188
|
+
return parsed?.mcpServers ?? {};
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Read stdio MCP servers from Claude Code's project-scoped config (~/.claude.json).
|
|
196
|
+
* Looks up projects[workspace].mcpServers for the agent's workspace path.
|
|
197
|
+
* Returns empty object if not found or on any error.
|
|
198
|
+
*/
|
|
199
|
+
readProjectScopedMcp() {
|
|
200
|
+
try {
|
|
201
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
202
|
+
const parsed = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
|
|
203
|
+
const projectServers = parsed?.projects?.[this.agentConfig.workspace]?.mcpServers;
|
|
204
|
+
return projectServers ?? {};
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
writeMcpConfig() {
|
|
211
|
+
if (this.source === 'api' && !this.agentConfig.allow_tools)
|
|
212
|
+
return null;
|
|
213
|
+
const stateDir = path.join(this.agentConfig.workspace, '.telegram-state');
|
|
214
|
+
const sessionDir = path.join(this.agentConfig.workspace, '.sessions', this.sessionId);
|
|
215
|
+
fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
|
216
|
+
const mcpServerPath = path.resolve(__dirname, '..', '..', 'mcp', 'server.ts');
|
|
217
|
+
// Merge stdio servers from Claude Code user + project configs (project overrides user).
|
|
218
|
+
// Skip "telegram" and "gateway" from both — gateway always generates its own config below.
|
|
219
|
+
const userServers = this.readUserScopedMcp();
|
|
220
|
+
const projectServers = this.readProjectScopedMcp();
|
|
221
|
+
const extraServers = {};
|
|
222
|
+
for (const [name, server] of Object.entries({ ...userServers, ...projectServers })) {
|
|
223
|
+
if (name !== 'telegram' && name !== 'gateway')
|
|
224
|
+
extraServers[name] = server;
|
|
225
|
+
}
|
|
226
|
+
const mcpConfig = {
|
|
227
|
+
mcpServers: {
|
|
228
|
+
...extraServers,
|
|
229
|
+
// Gateway always wins — must stay last to override any accidental collision
|
|
230
|
+
gateway: {
|
|
231
|
+
command: 'bun',
|
|
232
|
+
args: [mcpServerPath],
|
|
233
|
+
env: {
|
|
234
|
+
TELEGRAM_BOT_TOKEN: this.agentConfig.telegram?.botToken ?? '',
|
|
235
|
+
TELEGRAM_STATE_DIR: stateDir,
|
|
236
|
+
TELEGRAM_SEND_ONLY: 'true', // ALWAYS — session subprocesses never poll
|
|
237
|
+
DISCORD_BOT_TOKEN: this.agentConfig.discord?.botToken ?? '',
|
|
238
|
+
DISCORD_STATE_DIR: path.join(this.agentConfig.workspace, '.discord-state'),
|
|
239
|
+
DISCORD_GUILD_ALLOWLIST: (this.agentConfig.discord?.guildAllowlist ?? []).join(','),
|
|
240
|
+
DISCORD_CHANNEL_ALLOWLIST: (this.agentConfig.discord?.channelAllowlist ?? []).join(','),
|
|
241
|
+
DISCORD_DM_POLICY: this.agentConfig.discord?.dmPolicy ?? 'disabled',
|
|
242
|
+
DISCORD_DM_ALLOWLIST: (this.agentConfig.discord?.dmAllowlist ?? []).join(','),
|
|
243
|
+
GATEWAY_AGENT_ID: this.agentConfig.id,
|
|
244
|
+
GATEWAY_API_URL: process.env.GATEWAY_API_URL ?? `http://127.0.0.1:${process.env.PORT ?? '3000'}`,
|
|
245
|
+
GATEWAY_API_KEY: this.findApiKeyForAgent(this.agentConfig.id),
|
|
246
|
+
GATEWAY_ORIGIN_CHANNEL: this.source,
|
|
247
|
+
GATEWAY_WORKSPACE_DIR: this.agentConfig.workspace,
|
|
248
|
+
GATEWAY_SHARED_SKILLS_DIR: path.join(os.homedir(), '.claude-gateway', 'shared-skills'),
|
|
249
|
+
GATEWAY_SESSION_ID: this.sessionId,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const configPath = path.join(sessionDir, 'mcp-config.json');
|
|
255
|
+
fs.writeFileSync(configPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
|
|
256
|
+
const serverNames = Object.keys(mcpConfig.mcpServers);
|
|
257
|
+
this.logger.debug('MCP config written', { sessionId: this.sessionId, servers: serverNames });
|
|
258
|
+
return configPath;
|
|
259
|
+
}
|
|
260
|
+
/** Find the first API key that has access to this agent (agents: '*' or includes agentId). */
|
|
261
|
+
findApiKeyForAgent(agentId) {
|
|
262
|
+
const keys = this.gatewayConfig.gateway.api?.keys;
|
|
263
|
+
if (!keys?.length)
|
|
264
|
+
return '';
|
|
265
|
+
const match = keys.find(k => k.agents === '*' || (Array.isArray(k.agents) && k.agents.includes(agentId)));
|
|
266
|
+
return match?.key ?? '';
|
|
267
|
+
}
|
|
268
|
+
buildArgs(mcpConfigPath, model) {
|
|
269
|
+
const args = [
|
|
270
|
+
'--model', model,
|
|
271
|
+
'--input-format', 'stream-json',
|
|
272
|
+
'--output-format', 'stream-json',
|
|
273
|
+
'--include-partial-messages',
|
|
274
|
+
'--print',
|
|
275
|
+
'--verbose',
|
|
276
|
+
];
|
|
277
|
+
if (mcpConfigPath) {
|
|
278
|
+
// NOTE: --strict-mcp-config is intentionally omitted.
|
|
279
|
+
// With --strict-mcp-config, Claude Code blocks all plugin MCP servers (e.g. figma).
|
|
280
|
+
// Without it, enabled plugins (figma, etc.) load automatically alongside --mcp-config.
|
|
281
|
+
args.unshift('--mcp-config', mcpConfigPath);
|
|
282
|
+
}
|
|
283
|
+
if (this.agentConfig.claude.dangerouslySkipPermissions) {
|
|
284
|
+
args.push('--dangerously-skip-permissions');
|
|
285
|
+
}
|
|
286
|
+
for (const flag of this.agentConfig.claude.extraFlags ?? []) {
|
|
287
|
+
args.push(flag);
|
|
288
|
+
}
|
|
289
|
+
return args;
|
|
290
|
+
// NOTE: NO --channels flag — messages arrive via stdin injection, not Telegram channels
|
|
291
|
+
}
|
|
292
|
+
static toStreamJsonTurn(text) {
|
|
293
|
+
return JSON.stringify({
|
|
294
|
+
type: 'user',
|
|
295
|
+
message: { role: 'user', content: [{ type: 'text', text }] },
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async spawnProcess() {
|
|
299
|
+
const { prompt: initialPrompt, loadedAtSpawn, archivedCount, messageCountAtSpawn } = await this.buildInitialPrompt();
|
|
300
|
+
this.spawnContext = { loadedAtSpawn, archivedCount, messageCountAtSpawn };
|
|
301
|
+
const mcpConfigPath = this.writeMcpConfig();
|
|
302
|
+
const freshModel = this.readFreshModel();
|
|
303
|
+
const args = this.buildArgs(mcpConfigPath, freshModel);
|
|
304
|
+
const claudeBinRaw = process.env.CLAUDE_BIN ?? 'claude';
|
|
305
|
+
const claudeBinParts = claudeBinRaw.split(' ');
|
|
306
|
+
const claudeBin = claudeBinParts[0];
|
|
307
|
+
const allArgs = [...claudeBinParts.slice(1), ...args];
|
|
308
|
+
this.logger.info('Spawning session subprocess', {
|
|
309
|
+
sessionId: this.sessionId,
|
|
310
|
+
source: this.source,
|
|
311
|
+
});
|
|
312
|
+
const proc = (0, child_process_1.spawn)(claudeBin, allArgs, {
|
|
313
|
+
env: {
|
|
314
|
+
...process.env,
|
|
315
|
+
CLAUDE_WORKSPACE: this.agentConfig.workspace,
|
|
316
|
+
TELEGRAM_BOT_TOKEN: this.agentConfig.telegram?.botToken ?? '',
|
|
317
|
+
GATEWAY_RESTART_SIGNAL_PATH: this.restartSignalPath,
|
|
318
|
+
},
|
|
319
|
+
cwd: this.agentConfig.workspace,
|
|
320
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
321
|
+
});
|
|
322
|
+
this.process = proc;
|
|
323
|
+
// Send initial prompt only for Telegram sessions.
|
|
324
|
+
// API sessions receive the first message directly via sendApiMessage(),
|
|
325
|
+
// so no activation prompt is needed and sending one would race with
|
|
326
|
+
// the first API turn, causing sendApiMessage to resolve with the wrong result.
|
|
327
|
+
if (this.source !== 'api') {
|
|
328
|
+
proc.stdin?.write(SessionProcess.toStreamJsonTurn(initialPrompt) + '\n');
|
|
329
|
+
}
|
|
330
|
+
// Capture stdout — emit output events + persist assistant replies
|
|
331
|
+
const stateDir = this.source === 'discord'
|
|
332
|
+
? path.join(this.agentConfig.workspace, '.discord-state')
|
|
333
|
+
: path.join(this.agentConfig.workspace, '.telegram-state');
|
|
334
|
+
const typingDir = path.join(stateDir, 'typing');
|
|
335
|
+
const heartbeatPath = this.source !== 'api'
|
|
336
|
+
? path.join(typingDir, `${this.chatId}.heartbeat`)
|
|
337
|
+
: null;
|
|
338
|
+
const statusPath = this.source !== 'api'
|
|
339
|
+
? path.join(typingDir, `${this.chatId}.status`)
|
|
340
|
+
: null;
|
|
341
|
+
const writeStatus = (status, detail) => {
|
|
342
|
+
if (statusPath) {
|
|
343
|
+
const payload = detail
|
|
344
|
+
? JSON.stringify({ status, detail })
|
|
345
|
+
: status;
|
|
346
|
+
try {
|
|
347
|
+
fs.writeFileSync(statusPath, payload);
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const CODING_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit', 'MultiEdit']);
|
|
353
|
+
const TOOL_LABELS = {
|
|
354
|
+
Read: { emoji: '📖', verb: 'Reading' },
|
|
355
|
+
Edit: { emoji: '✏️', verb: 'Editing' },
|
|
356
|
+
Write: { emoji: '📝', verb: 'Writing' },
|
|
357
|
+
MultiEdit: { emoji: '❤️🔥', verb: 'Editing' },
|
|
358
|
+
NotebookEdit: { emoji: '📕', verb: 'Editing notebook' },
|
|
359
|
+
Grep: { emoji: '🔦', verb: 'Searching for' },
|
|
360
|
+
Glob: { emoji: '📂', verb: 'Finding files' },
|
|
361
|
+
Bash: { emoji: '💻', verb: 'Running' },
|
|
362
|
+
WebFetch: { emoji: '🌐', verb: 'Fetching' },
|
|
363
|
+
WebSearch: { emoji: '🔎', verb: 'Searching' },
|
|
364
|
+
Agent: { emoji: '🤖', verb: 'Running agent' },
|
|
365
|
+
Task: { emoji: '👉', verb: 'Running task' },
|
|
366
|
+
TodoWrite: { emoji: '📋', verb: 'Updating tasks' },
|
|
367
|
+
};
|
|
368
|
+
function shortenPath(p) {
|
|
369
|
+
// Keep last 2 segments for context (e.g. "src/api-router.ts" not just "api-router.ts")
|
|
370
|
+
const parts = p.split('/');
|
|
371
|
+
return parts.length > 2 ? parts.slice(-2).join('/') : parts[parts.length - 1] || p;
|
|
372
|
+
}
|
|
373
|
+
function truncateDetail(s, maxLines = 5, maxChars = 300) {
|
|
374
|
+
const lines = s.split('\n').filter(l => l.trim());
|
|
375
|
+
const trimmedByLines = lines.length > maxLines;
|
|
376
|
+
const kept = lines.slice(0, maxLines);
|
|
377
|
+
let result = kept.join('\n');
|
|
378
|
+
if (result.length > maxChars) {
|
|
379
|
+
result = result.slice(0, maxChars) + '...';
|
|
380
|
+
}
|
|
381
|
+
else if (trimmedByLines) {
|
|
382
|
+
result += '\n...';
|
|
383
|
+
}
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
function extractToolDetail(name, input) {
|
|
387
|
+
const label = TOOL_LABELS[name] ?? { emoji: '🔧', verb: name };
|
|
388
|
+
const { emoji, verb } = label;
|
|
389
|
+
// Build context parts based on tool type
|
|
390
|
+
switch (name) {
|
|
391
|
+
case 'Read':
|
|
392
|
+
case 'Edit':
|
|
393
|
+
case 'Write':
|
|
394
|
+
case 'MultiEdit':
|
|
395
|
+
case 'NotebookEdit': {
|
|
396
|
+
const file = typeof input.file_path === 'string' ? shortenPath(input.file_path) : '';
|
|
397
|
+
const desc = typeof input.description === 'string' ? ` — ${input.description}` : '';
|
|
398
|
+
return truncateDetail(`${emoji} ${verb}: ${file}${desc}`);
|
|
399
|
+
}
|
|
400
|
+
case 'Grep': {
|
|
401
|
+
const pattern = typeof input.pattern === 'string' ? `"${input.pattern}"` : '';
|
|
402
|
+
const path = typeof input.path === 'string' ? ` in ${shortenPath(input.path)}` : '';
|
|
403
|
+
return truncateDetail(`${emoji} ${verb}: ${pattern}${path}`);
|
|
404
|
+
}
|
|
405
|
+
case 'Glob': {
|
|
406
|
+
const pattern = typeof input.pattern === 'string' ? `"${input.pattern}"` : '';
|
|
407
|
+
return truncateDetail(`${emoji} ${verb}: ${pattern}`);
|
|
408
|
+
}
|
|
409
|
+
case 'Bash': {
|
|
410
|
+
const desc = typeof input.description === 'string' ? input.description : '';
|
|
411
|
+
const cmd = typeof input.command === 'string' ? input.command : '';
|
|
412
|
+
return truncateDetail(`${emoji} ${verb}: ${desc || cmd}`);
|
|
413
|
+
}
|
|
414
|
+
case 'WebFetch': {
|
|
415
|
+
const url = typeof input.url === 'string' ? input.url : '';
|
|
416
|
+
return truncateDetail(`${emoji} ${verb}: ${url}`);
|
|
417
|
+
}
|
|
418
|
+
case 'WebSearch': {
|
|
419
|
+
const query = typeof input.query === 'string' ? input.query : '';
|
|
420
|
+
return truncateDetail(`${emoji} ${verb}: "${query}"`);
|
|
421
|
+
}
|
|
422
|
+
case 'Agent':
|
|
423
|
+
case 'Task': {
|
|
424
|
+
const desc = typeof input.description === 'string' ? input.description : '';
|
|
425
|
+
const prompt = typeof input.prompt === 'string' ? input.prompt : '';
|
|
426
|
+
return truncateDetail(`${emoji} ${verb}: ${desc || prompt}`);
|
|
427
|
+
}
|
|
428
|
+
case 'TodoWrite': {
|
|
429
|
+
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
430
|
+
const active = todos.find(t => t.status === 'in_progress');
|
|
431
|
+
const detail = active?.content ?? `${todos.length} items`;
|
|
432
|
+
return truncateDetail(`${emoji} ${verb}: ${detail}`);
|
|
433
|
+
}
|
|
434
|
+
default: {
|
|
435
|
+
// Generic fallback: try description, then name
|
|
436
|
+
const desc = typeof input.description === 'string' ? input.description : '';
|
|
437
|
+
return truncateDetail(`${emoji} ${verb}: ${desc || '...'}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
let assistantBuffer = '';
|
|
442
|
+
// Track partial message text to avoid double-counting when --include-partial-messages is active.
|
|
443
|
+
// Each partial `type: 'assistant'` event contains the FULL text so far, not a delta.
|
|
444
|
+
let lastPartialText = '';
|
|
445
|
+
// Track context from message_start events (first sub-call of each turn) for accurate context % display.
|
|
446
|
+
// result.usage is cumulative across all sub-calls; message_start.usage reflects a single API call's context.
|
|
447
|
+
let lastMessageStartContext = 0;
|
|
448
|
+
proc.stdout?.on('data', (data) => {
|
|
449
|
+
// Update heartbeat so the receiver's stalled detector knows Claude is active
|
|
450
|
+
if (heartbeatPath) {
|
|
451
|
+
try {
|
|
452
|
+
fs.writeFileSync(heartbeatPath, String(Date.now()));
|
|
453
|
+
}
|
|
454
|
+
catch { }
|
|
455
|
+
}
|
|
456
|
+
const lines = data.toString().split('\n');
|
|
457
|
+
for (const line of lines) {
|
|
458
|
+
if (!line.trim())
|
|
459
|
+
continue;
|
|
460
|
+
this.emit('output', line);
|
|
461
|
+
this.logger.debug('session output', { line });
|
|
462
|
+
// Try to capture assistant text for SessionStore + update status file
|
|
463
|
+
try {
|
|
464
|
+
const obj = JSON.parse(line);
|
|
465
|
+
// stream-json assistant message (partial or final)
|
|
466
|
+
if (obj.type === 'assistant' && Array.isArray(obj.message?.content)) {
|
|
467
|
+
// Extract full text from all text blocks in this message
|
|
468
|
+
let fullText = '';
|
|
469
|
+
for (const block of obj.message.content) {
|
|
470
|
+
if (block.type === 'text')
|
|
471
|
+
fullText += block.text;
|
|
472
|
+
}
|
|
473
|
+
const isPartial = obj.stop_reason === null || obj.stop_reason === undefined;
|
|
474
|
+
if (isPartial) {
|
|
475
|
+
// Partial message: update buffer with only the delta
|
|
476
|
+
// (fullText is cumulative, so delta = new portion)
|
|
477
|
+
if (fullText.length > lastPartialText.length) {
|
|
478
|
+
const delta = fullText.slice(lastPartialText.length);
|
|
479
|
+
if (this.queryMode) {
|
|
480
|
+
this._queryBuffer += delta;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
assistantBuffer += delta;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
lastPartialText = fullText;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// Final message: use full text, reset partial tracking
|
|
490
|
+
if (fullText.length > lastPartialText.length) {
|
|
491
|
+
const delta = fullText.slice(lastPartialText.length);
|
|
492
|
+
if (this.queryMode) {
|
|
493
|
+
this._queryBuffer += delta;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
assistantBuffer += delta;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
lastPartialText = '';
|
|
500
|
+
}
|
|
501
|
+
if (!this.queryMode) {
|
|
502
|
+
// Detect tool use to write status (same as before)
|
|
503
|
+
const toolBlock = obj.message.content.find((b) => b.type === 'tool_use');
|
|
504
|
+
if (toolBlock) {
|
|
505
|
+
const detail = extractToolDetail(toolBlock.name ?? '', toolBlock.input ?? {});
|
|
506
|
+
writeStatus(CODING_TOOLS.has(toolBlock.name ?? '') ? 'coding' : 'tool', detail);
|
|
507
|
+
}
|
|
508
|
+
else if (!isPartial && obj.message.content.some((b) => b.type === 'text')) {
|
|
509
|
+
// Only update thinking status on final messages, not every partial
|
|
510
|
+
const textBlock = obj.message.content.find((b) => b.type === 'text');
|
|
511
|
+
const textSnippet = textBlock?.text ? truncateDetail(`🧠 ${textBlock.text}`) : undefined;
|
|
512
|
+
writeStatus('thinking', textSnippet);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// task_started / task_progress
|
|
517
|
+
if (obj.type === 'system' && (obj.subtype === 'task_started' || obj.subtype === 'task_progress')) {
|
|
518
|
+
const taskDesc = typeof obj.description === 'string' ? obj.description : '';
|
|
519
|
+
if (obj.subtype === 'task_started') {
|
|
520
|
+
writeStatus('tool', truncateDetail(`🤖 ${taskDesc}`));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
const toolName = typeof obj.last_tool_name === 'string' ? obj.last_tool_name : '';
|
|
524
|
+
const toolLabel = TOOL_LABELS[toolName] ?? { emoji: '🔧', verb: toolName };
|
|
525
|
+
writeStatus('tool', truncateDetail(`${toolLabel.emoji} ${taskDesc}`));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// rate_limit_event
|
|
529
|
+
if (obj.type === 'rate_limit_event') {
|
|
530
|
+
writeStatus('waiting', '⏳ Rate limited, retrying...');
|
|
531
|
+
}
|
|
532
|
+
// text delta (standalone, not from assistant messages)
|
|
533
|
+
if (obj.type === 'text') {
|
|
534
|
+
if (this.queryMode) {
|
|
535
|
+
this._queryBuffer += obj.text ?? '';
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
assistantBuffer += obj.text ?? '';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Capture context size from message_start (first sub-call of each turn)
|
|
542
|
+
if (obj.type === 'stream_event' && obj.event?.type === 'message_start') {
|
|
543
|
+
const msUsage = obj.event.message?.usage;
|
|
544
|
+
if (msUsage) {
|
|
545
|
+
lastMessageStartContext = (msUsage.input_tokens ?? 0) + (msUsage.cache_read_input_tokens ?? 0) + (msUsage.cache_creation_input_tokens ?? 0);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// result = end of turn
|
|
549
|
+
if (obj.type === 'result') {
|
|
550
|
+
lastPartialText = ''; // reset for next turn
|
|
551
|
+
writeStatus(obj.is_error ? 'error' : 'done');
|
|
552
|
+
if (this.queryMode) {
|
|
553
|
+
if (this._queryTimer)
|
|
554
|
+
clearTimeout(this._queryTimer);
|
|
555
|
+
if (!this._querySettled) {
|
|
556
|
+
this._querySettled = true;
|
|
557
|
+
const resolve = this._queryResolve;
|
|
558
|
+
this.queryMode = false;
|
|
559
|
+
this._queryResolve = undefined;
|
|
560
|
+
resolve?.(this._queryBuffer.trim());
|
|
561
|
+
}
|
|
562
|
+
this._queryBuffer = '';
|
|
563
|
+
assistantBuffer = '';
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
if (assistantBuffer.trim()) {
|
|
567
|
+
const assistantMsg = { role: 'assistant', content: assistantBuffer.trim(), ts: Date.now() };
|
|
568
|
+
const appendAssistant = this.source !== 'api'
|
|
569
|
+
? this.sessionStore.appendTelegramMessage(this.agentConfig.id, this.chatId, this.sessionId, assistantMsg, this.sessionChannel)
|
|
570
|
+
: this.sessionStore.appendMessage(this.agentConfig.id, this.sessionId, assistantMsg);
|
|
571
|
+
appendAssistant.catch(() => { });
|
|
572
|
+
assistantBuffer = '';
|
|
573
|
+
}
|
|
574
|
+
// Emit tokenUsage using message_start context (accurate per-call context window usage)
|
|
575
|
+
// rather than result.usage which is cumulative across all sub-calls in the turn.
|
|
576
|
+
const usage = obj.usage;
|
|
577
|
+
const outputTokens = usage?.output_tokens ?? 0;
|
|
578
|
+
const totalTokens = lastMessageStartContext + outputTokens;
|
|
579
|
+
if (lastMessageStartContext > 0) {
|
|
580
|
+
this.emit('tokenUsage', { inputTokens: lastMessageStartContext, outputTokens, totalTokens });
|
|
581
|
+
}
|
|
582
|
+
lastMessageStartContext = 0;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
/* not JSON */
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
proc.stderr?.on('data', (data) => {
|
|
592
|
+
this.logger.warn('session stderr', { stderr: data.toString() });
|
|
593
|
+
});
|
|
594
|
+
proc.on('exit', (code, signal) => {
|
|
595
|
+
this.logger.info('session subprocess exited', {
|
|
596
|
+
code,
|
|
597
|
+
signal,
|
|
598
|
+
sessionId: this.sessionId,
|
|
599
|
+
});
|
|
600
|
+
this.process = null;
|
|
601
|
+
if (!this.stopping)
|
|
602
|
+
this.scheduleRestart();
|
|
603
|
+
});
|
|
604
|
+
proc.on('error', (err) => {
|
|
605
|
+
this.logger.error('session subprocess error', { error: err.message });
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
scheduleRestart() {
|
|
609
|
+
// Graceful self-restart requested by agent — reset counter so it doesn't
|
|
610
|
+
// count against MAX_RESTARTS (this is an intentional restart, not a crash).
|
|
611
|
+
if (this.restartRequested) {
|
|
612
|
+
this.restartRequested = false;
|
|
613
|
+
this.restartCount = 0;
|
|
614
|
+
}
|
|
615
|
+
if (this.restartCount >= MAX_RESTARTS) {
|
|
616
|
+
this.logger.error('Session max restarts reached', { sessionId: this.sessionId });
|
|
617
|
+
this.emit('failed');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
this.restartCount++;
|
|
621
|
+
this.logger.warn(`Scheduling session restart in ${AUTO_RESTART_DELAY_MS}ms`, {
|
|
622
|
+
attempt: this.restartCount,
|
|
623
|
+
});
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
if (!this.stopping) {
|
|
626
|
+
this.spawnProcess().catch(err => this.logger.error('restart failed', { error: err.message }));
|
|
627
|
+
}
|
|
628
|
+
}, AUTO_RESTART_DELAY_MS);
|
|
629
|
+
}
|
|
630
|
+
sendMessage(text) {
|
|
631
|
+
if (!this.process?.stdin?.writable) {
|
|
632
|
+
this.logger.warn('Cannot send message: subprocess not running', {
|
|
633
|
+
sessionId: this.sessionId,
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
// Signal queued state so the typing loop can update the reaction immediately
|
|
638
|
+
if (this.source !== 'api') {
|
|
639
|
+
const stateSubDir = this.source === 'discord' ? '.discord-state' : '.telegram-state';
|
|
640
|
+
const statusPath = path.join(this.agentConfig.workspace, stateSubDir, 'typing', `${this.sessionId}.status`);
|
|
641
|
+
try {
|
|
642
|
+
fs.writeFileSync(statusPath, 'queued');
|
|
643
|
+
}
|
|
644
|
+
catch { }
|
|
645
|
+
}
|
|
646
|
+
this.process.stdin.write(SessionProcess.toStreamJsonTurn(text) + '\n');
|
|
647
|
+
}
|
|
648
|
+
query(prompt, timeoutMs = 60000) {
|
|
649
|
+
return new Promise((resolve, reject) => {
|
|
650
|
+
if (!this.process?.stdin?.writable) {
|
|
651
|
+
reject(new Error('Cannot query: subprocess not running'));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this._querySettled = false;
|
|
655
|
+
this._queryResolve = resolve;
|
|
656
|
+
this._queryBuffer = '';
|
|
657
|
+
this.queryMode = true;
|
|
658
|
+
this._queryTimer = setTimeout(() => {
|
|
659
|
+
if (this._querySettled)
|
|
660
|
+
return;
|
|
661
|
+
this._querySettled = true;
|
|
662
|
+
this.queryMode = false;
|
|
663
|
+
this._queryResolve = undefined;
|
|
664
|
+
reject(new Error('query timeout'));
|
|
665
|
+
}, timeoutMs);
|
|
666
|
+
this.sendMessage(prompt);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
get isProcessing() { return this._processing; }
|
|
670
|
+
setProcessing(active) {
|
|
671
|
+
if (this._processing !== active) {
|
|
672
|
+
this._processing = active;
|
|
673
|
+
this.emit('processingChange', active);
|
|
674
|
+
if (!active && this._pendingRestart) {
|
|
675
|
+
this.emit('deferredRestartReady');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
interrupt() {
|
|
680
|
+
if (!this.process || this.process.killed)
|
|
681
|
+
return false;
|
|
682
|
+
if (!this._processing)
|
|
683
|
+
return false;
|
|
684
|
+
this.process.kill('SIGINT');
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
markPendingRestart() {
|
|
688
|
+
if (!this._processing) {
|
|
689
|
+
this.emit('deferredRestartReady');
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
this._pendingRestart = true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
touch() {
|
|
696
|
+
this.lastActivityAt = Date.now();
|
|
697
|
+
}
|
|
698
|
+
isIdle(idleMs) {
|
|
699
|
+
return Date.now() - this.lastActivityAt > idleMs;
|
|
700
|
+
}
|
|
701
|
+
isRunning() {
|
|
702
|
+
return this.process !== null && !this.process.killed;
|
|
703
|
+
}
|
|
704
|
+
async stop() {
|
|
705
|
+
this.stopping = true;
|
|
706
|
+
await this.restartWatcher?.close();
|
|
707
|
+
this.restartWatcher = null;
|
|
708
|
+
try {
|
|
709
|
+
fs.rmSync(this.restartSignalPath, { force: true });
|
|
710
|
+
}
|
|
711
|
+
catch { }
|
|
712
|
+
if (!this.process)
|
|
713
|
+
return;
|
|
714
|
+
return new Promise((resolve) => {
|
|
715
|
+
const proc = this.process;
|
|
716
|
+
proc.once('exit', () => {
|
|
717
|
+
this.process = null;
|
|
718
|
+
resolve();
|
|
719
|
+
});
|
|
720
|
+
proc.kill('SIGTERM');
|
|
721
|
+
setTimeout(() => {
|
|
722
|
+
if (this.process)
|
|
723
|
+
proc.kill('SIGKILL');
|
|
724
|
+
}, 10000);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
exports.SessionProcess = SessionProcess;
|
|
729
|
+
//# sourceMappingURL=process.js.map
|