Files
startup/server/sessionManager.js

221 lines
5.8 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 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. 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.state = {
status: "idle",
startedAt: null,
teamActivated: false,
lastError: null,
runtime: getPublicRuntimeConfig(config)
};
}
getState() {
return {
...this.state,
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();
}
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.config.workspaceDir,
env: getClaudeEnv(this.config)
});
this.setState({
status: "starting",
startedAt: new Date().toISOString(),
teamActivated: false,
lastError: null
});
this.addLog("lifecycle", `Starting Claude session in ${this.config.workspaceDir}`);
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.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;
}
}