Files
startup/server/sessionManager.js

266 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "fs";
import path from "path";
import stripAnsi from "strip-ansi";
import { buildBootstrapPrompt } from "./bootstrapPrompt.js";
import { LogService } from "./logService.js";
import { PtyService } from "./ptyService.js";
import { getClaudeEnv, getPublicRuntimeConfig } from "./config.js";
import { findMentionedMember } from "./teamConfig.js";
function cleanChunk(value) {
return stripAnsi(value).replace(/\r/g, "");
}
function isLikelyFollowUp(prompt) {
const normalized = String(prompt ?? "").trim();
const wordCount = normalized.split(/\s+/).filter(Boolean).length;
return [
wordCount <= 8,
/^(evet|hayir|tamam|olur|olsun|sade|sekersiz|şekersiz|detaylandir|detaylandır|devam|peki|neden|nasil|nasıl)\b/i.test(normalized)
].some(Boolean);
}
function buildRoutedPrompt(prompt, lastDirectedMember = null) {
const explicitTarget = findMentionedMember(prompt);
const targetMember = explicitTarget ?? (lastDirectedMember && isLikelyFollowUp(prompt) ? lastDirectedMember : null);
if (!targetMember) {
return {
targetMember: null,
routedPrompt: `Not: Bu genel mesajdir. Once Mazlum cevap versin ve konusan herkes ad etiketi kullansin. Irgatov teknik gorev almaz; sadece kahve ve lojistik destek verir. Kullanici mesaji: ${prompt}`
};
}
if (targetMember.name === "Irgatov") {
return {
targetMember,
routedPrompt: `Not: Bu mesaj Irgatov icindir. Irgatov sadece kahve, icecek, servis ve basit lojistik destek konularinda cevap versin. Teknik plan, kod, mimari veya dosya yapisi onermesin. Cevap \`Irgatov:\` ile baslasin. Kullanici mesaji: ${prompt}`
};
}
return {
targetMember,
routedPrompt: `Not: Bu mesaj ${targetMember.name} icindir. Yalnizca ${targetMember.name} cevap versin ve cevap \`${targetMember.name}:\` ile baslasin. Kullanici mesaji: ${prompt}`
};
}
export class SessionManager {
constructor({ io, config }) {
this.io = io;
this.config = config;
this.logService = new LogService(config.watchLogLimit, config.logToConsole ? console : null);
this.ptyService = null;
this.chatOutput = "";
this.lastDirectedMember = null;
this.currentProjectPath = null;
this.state = {
status: "idle",
startedAt: null,
teamActivated: false,
lastError: null,
currentProjectPath: null,
runtime: getPublicRuntimeConfig(config)
};
}
getState() {
return {
...this.state,
currentProjectPath: this.currentProjectPath,
runtime: getPublicRuntimeConfig(this.config)
};
}
getLogSnapshot() {
return this.logService.snapshot();
}
getChatSnapshot() {
return this.chatOutput;
}
emitState() {
this.io.emit("session:state", this.getState());
}
emitLog(entry) {
this.io.emit("log:entry", entry);
}
emitChat(chunk) {
this.io.emit("chat:chunk", { chunk });
}
addLog(type, message, meta = {}) {
const entry = this.logService.push(type, message, meta);
this.emitLog(entry);
return entry;
}
setState(patch) {
this.state = {
...this.state,
...patch
};
this.emitState();
}
getActiveWorkspaceDir() {
return this.currentProjectPath ?? this.config.workspaceDir;
}
async setProjectPath(projectPath) {
const resolved = projectPath ? path.resolve(projectPath) : null;
const wasRunning = this.ptyService?.isRunning() ?? false;
if (resolved && (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory())) {
throw new Error(`Selected project path is invalid: ${resolved}`);
}
if (wasRunning) {
await this.stop();
}
this.currentProjectPath = resolved;
this.lastDirectedMember = null;
this.setState({
currentProjectPath: resolved,
teamActivated: false
});
this.addLog("system", `Current project set to ${resolved ?? "None"}`);
if (wasRunning) {
await this.start();
if (resolved) {
await this.activateTeam();
}
}
}
async start() {
if (this.ptyService?.isRunning()) {
throw new Error("Session is already running");
}
this.chatOutput = "";
this.lastDirectedMember = null;
this.logService.clear();
this.io.emit("chat:reset");
this.ptyService = new PtyService({
cwd: this.getActiveWorkspaceDir(),
env: getClaudeEnv(this.config)
});
this.setState({
status: "starting",
startedAt: new Date().toISOString(),
teamActivated: false,
lastError: null
});
this.addLog("lifecycle", `Starting Claude session in ${this.getActiveWorkspaceDir()}`);
try {
await this.ptyService.start({
command: this.config.claudeBin,
args: this.config.claudeArgs,
onData: (chunk) => this.handlePtyData(chunk),
onExit: (event) => this.handlePtyExit(event)
});
this.setState({ status: "running" });
this.addLog("lifecycle", `Claude process started with binary ${this.config.claudeBin}`);
} catch (error) {
this.setState({
status: "error",
lastError: error.message
});
this.addLog("error", error.message);
throw error;
}
}
async stop() {
if (!this.ptyService?.isRunning()) {
return;
}
this.addLog("lifecycle", "Stopping Claude session");
await this.ptyService.stop();
this.lastDirectedMember = null;
this.setState({
status: "stopped",
teamActivated: false
});
}
async sendPrompt(prompt) {
if (!this.ptyService?.isRunning()) {
throw new Error("Session is not running");
}
const { routedPrompt, targetMember } = buildRoutedPrompt(prompt, this.lastDirectedMember);
const input = `${routedPrompt}\r`;
this.lastDirectedMember = targetMember ?? null;
this.addLog("input", prompt);
await this.ptyService.write(input);
}
async sendRawPrompt(prompt, meta = {}) {
if (!this.ptyService?.isRunning()) {
throw new Error("Session is not running");
}
const input = `${prompt}\r`;
this.addLog("input", meta.label ?? prompt, meta);
await this.ptyService.write(input);
}
async activateTeam() {
const prompt = buildBootstrapPrompt(this.currentProjectPath);
this.lastDirectedMember = null;
await this.sendRawPrompt(prompt, { label: "[bootstrap] Team activation prompt sent" });
this.setState({ teamActivated: true });
this.addLog("system", "Team activation prompt sent");
}
resize({ cols, rows }) {
this.ptyService?.resize(cols, rows);
}
clearLogs() {
this.logService.clear();
this.io.emit("log:snapshot", []);
this.addLog("system", "Watch log cleared");
}
handlePtyData(chunk) {
const clean = cleanChunk(chunk);
if (!clean) {
return;
}
this.chatOutput += clean;
if (this.chatOutput.length > this.config.chatChunkLimit * 20) {
this.chatOutput = this.chatOutput.slice(-this.config.chatChunkLimit * 20);
}
this.emitChat(clean);
this.addLog("output", clean);
}
handlePtyExit(event) {
const exitCode = event?.exitCode ?? 0;
const signal = event?.signal ?? 0;
const detail = event?.error ? `, error=${event.error}` : "";
this.addLog("lifecycle", `Claude session exited (code=${exitCode}, signal=${signal}${detail})`);
this.setState({
status: "stopped",
teamActivated: false
});
this.ptyService = null;
}
}