nemoris 0.1.0 → 0.1.2
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/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
|
@@ -1,411 +1,411 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import net from "node:net";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { TelegramInboundHandler, resolveStatusModel } from "./telegram-inbound.js";
|
|
6
|
-
|
|
7
|
-
const SESSION_PREFIX = "tui";
|
|
8
|
-
const SESSION_REGISTRY = new Map();
|
|
9
|
-
const SOCKET_STATE = new WeakMap();
|
|
10
|
-
let nextJobCounter = 1;
|
|
11
|
-
|
|
12
|
-
function safeUnlink(filePath) {
|
|
13
|
-
try {
|
|
14
|
-
fs.unlinkSync(filePath);
|
|
15
|
-
} catch (error) {
|
|
16
|
-
if (error?.code !== "ENOENT") throw error;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function normalizeText(value) {
|
|
21
|
-
return typeof value === "string" ? value : String(value ?? "");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function writeNdjson(socket, payload) {
|
|
25
|
-
if (!socket || socket.destroyed || !socket.writable) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
socket.write(`${JSON.stringify(payload)}\n`);
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function resolveRequestedAgent(runtime, requestedAgent, fallbackAgent) {
|
|
33
|
-
const configuredAgents = new Set(Object.keys(runtime?.agents || {}));
|
|
34
|
-
if (requestedAgent && configuredAgents.has(requestedAgent)) {
|
|
35
|
-
return requestedAgent;
|
|
36
|
-
}
|
|
37
|
-
if (fallbackAgent && configuredAgents.has(fallbackAgent)) {
|
|
38
|
-
return fallbackAgent;
|
|
39
|
-
}
|
|
40
|
-
const firstConfigured = configuredAgents.values().next().value;
|
|
41
|
-
return firstConfigured || requestedAgent || fallbackAgent || "main";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resolveApprovalId(gate, payload = {}) {
|
|
45
|
-
if (payload.approvalId) {
|
|
46
|
-
return String(payload.approvalId);
|
|
47
|
-
}
|
|
48
|
-
if (!payload.jobId) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
const pending = gate.getPending().find((entry) => entry.jobId === payload.jobId);
|
|
52
|
-
return pending?.approvalId || null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function resolveTuiSocketPath(stateRoot) {
|
|
56
|
-
if (process.platform === "win32") {
|
|
57
|
-
return "\\\\.\\pipe\\nemoris-daemon";
|
|
58
|
-
}
|
|
59
|
-
return path.join(stateRoot, "daemon.sock");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function buildTuiSessionKey(sessionId) {
|
|
63
|
-
return `${SESSION_PREFIX}:${sessionId}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function registerTuiSession(sessionKey, socket, meta = {}) {
|
|
67
|
-
const existing = SESSION_REGISTRY.get(sessionKey);
|
|
68
|
-
if (existing?.socket && existing.socket !== socket) {
|
|
69
|
-
existing.socket.destroy();
|
|
70
|
-
}
|
|
71
|
-
SESSION_REGISTRY.set(sessionKey, { socket, meta });
|
|
72
|
-
SOCKET_STATE.set(socket, { sessionKey, ...meta });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function unregisterTuiSession(socket) {
|
|
76
|
-
const state = SOCKET_STATE.get(socket);
|
|
77
|
-
if (!state?.sessionKey) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const current = SESSION_REGISTRY.get(state.sessionKey);
|
|
81
|
-
if (current?.socket === socket) {
|
|
82
|
-
SESSION_REGISTRY.delete(state.sessionKey);
|
|
83
|
-
}
|
|
84
|
-
SOCKET_STATE.delete(socket);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function getRegisteredTuiSession(sessionKey) {
|
|
88
|
-
return SESSION_REGISTRY.get(sessionKey) || null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function sendTuiEvent(sessionKey, payload) {
|
|
92
|
-
const entry = SESSION_REGISTRY.get(sessionKey);
|
|
93
|
-
if (!entry) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
return writeNdjson(entry.socket, payload);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function resetTuiSessionRegistry() {
|
|
100
|
-
for (const { socket } of SESSION_REGISTRY.values()) {
|
|
101
|
-
if (socket && !socket.destroyed) {
|
|
102
|
-
socket.destroy();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
SESSION_REGISTRY.clear();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export class TuiServer {
|
|
109
|
-
constructor({
|
|
110
|
-
stateStore,
|
|
111
|
-
stateRoot,
|
|
112
|
-
runtime,
|
|
113
|
-
contextLedger = null,
|
|
114
|
-
execApprovalGate = null,
|
|
115
|
-
logger = console,
|
|
116
|
-
} = {}) {
|
|
117
|
-
this.stateStore = stateStore;
|
|
118
|
-
this.stateRoot = stateRoot;
|
|
119
|
-
this.runtime = runtime || {};
|
|
120
|
-
this.contextLedger = contextLedger;
|
|
121
|
-
this.execApprovalGate = execApprovalGate || null;
|
|
122
|
-
this.logger = logger;
|
|
123
|
-
this.server = null;
|
|
124
|
-
this.socketPath = resolveTuiSocketPath(stateRoot);
|
|
125
|
-
this.defaultAgent = resolveRequestedAgent(
|
|
126
|
-
runtime,
|
|
127
|
-
runtime?.runtime?.telegram?.defaultAgent,
|
|
128
|
-
"main",
|
|
129
|
-
);
|
|
130
|
-
const agentNames = Object.fromEntries(
|
|
131
|
-
Object.entries(runtime?.agents || {}).map(([agentId, config]) => [agentId, config?.name || agentId]),
|
|
132
|
-
);
|
|
133
|
-
this.commandHandler = new TelegramInboundHandler({
|
|
134
|
-
stateStore,
|
|
135
|
-
telegramConfig: {
|
|
136
|
-
defaultAgent: this.defaultAgent,
|
|
137
|
-
operatorChatId: "",
|
|
138
|
-
authorizedChatIds: [],
|
|
139
|
-
pollingMode: false,
|
|
140
|
-
sessionLabelPrefix: SESSION_PREFIX,
|
|
141
|
-
},
|
|
142
|
-
availablePeers: Object.keys(runtime?.agents || {}),
|
|
143
|
-
contextLedger,
|
|
144
|
-
agentNames,
|
|
145
|
-
routerConfig: runtime?.router || null,
|
|
146
|
-
agentConfigs: runtime?.agents || null,
|
|
147
|
-
execApprovalGate,
|
|
148
|
-
logger,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
setExecApprovalGate(execApprovalGate) {
|
|
153
|
-
this.execApprovalGate = execApprovalGate || null;
|
|
154
|
-
this.commandHandler._execApprovalGate = execApprovalGate || null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async start() {
|
|
158
|
-
if (this.server) {
|
|
159
|
-
return { socketPath: this.socketPath };
|
|
160
|
-
}
|
|
161
|
-
if (process.platform !== "win32") {
|
|
162
|
-
safeUnlink(this.socketPath);
|
|
163
|
-
}
|
|
164
|
-
this.server = net.createServer((socket) => this._handleConnection(socket));
|
|
165
|
-
await new Promise((resolve, reject) => {
|
|
166
|
-
const onError = (error) => {
|
|
167
|
-
this.server?.off("listening", onListening);
|
|
168
|
-
reject(error);
|
|
169
|
-
};
|
|
170
|
-
const onListening = () => {
|
|
171
|
-
this.server?.off("error", onError);
|
|
172
|
-
resolve();
|
|
173
|
-
};
|
|
174
|
-
this.server.once("error", onError);
|
|
175
|
-
this.server.once("listening", onListening);
|
|
176
|
-
this.server.listen(this.socketPath);
|
|
177
|
-
});
|
|
178
|
-
return { socketPath: this.socketPath };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async close() {
|
|
182
|
-
if (!this.server) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const server = this.server;
|
|
186
|
-
this.server = null;
|
|
187
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
188
|
-
if (process.platform !== "win32") {
|
|
189
|
-
safeUnlink(this.socketPath);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async buildStatusPayload(sessionKey) {
|
|
194
|
-
const session = this.stateStore.getChatSession(sessionKey);
|
|
195
|
-
const usage = this.stateStore.db?.prepare(`
|
|
196
|
-
SELECT
|
|
197
|
-
COALESCE(SUM(tokens_in + tokens_out), 0) as tokens,
|
|
198
|
-
COALESCE(SUM(cost_usd), 0) as cost
|
|
199
|
-
FROM usage_log
|
|
200
|
-
WHERE session_id = ?
|
|
201
|
-
`).get(sessionKey) || { tokens: 0, cost: 0 };
|
|
202
|
-
return {
|
|
203
|
-
type: "status",
|
|
204
|
-
sessionKey,
|
|
205
|
-
agent: session?.agent_id || this.defaultAgent,
|
|
206
|
-
model: resolveStatusModel(session, {
|
|
207
|
-
routerConfig: this.runtime?.router || null,
|
|
208
|
-
agentConfigs: this.runtime?.agents || null,
|
|
209
|
-
}) || "default",
|
|
210
|
-
tokens: usage.tokens || 0,
|
|
211
|
-
cost: usage.cost || 0,
|
|
212
|
-
connected: true,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
_handleConnection(socket) {
|
|
217
|
-
socket.setEncoding("utf8");
|
|
218
|
-
let buffer = "";
|
|
219
|
-
|
|
220
|
-
socket.on("data", async (chunk) => {
|
|
221
|
-
buffer += chunk;
|
|
222
|
-
const lines = buffer.split("\n");
|
|
223
|
-
buffer = lines.pop() || "";
|
|
224
|
-
for (const rawLine of lines) {
|
|
225
|
-
const line = rawLine.trim();
|
|
226
|
-
if (!line) continue;
|
|
227
|
-
let payload;
|
|
228
|
-
try {
|
|
229
|
-
payload = JSON.parse(line);
|
|
230
|
-
} catch {
|
|
231
|
-
this._send(socket, { type: "error", message: "Invalid JSON payload.", code: "INVALID_JSON" });
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
await this._handlePayload(socket, payload);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
this.logger.error?.(`[tui-server] ${error.message}`);
|
|
238
|
-
this._send(socket, { type: "error", message: error.message, code: "TUI_SERVER_ERROR" });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
socket.on("close", () => unregisterTuiSession(socket));
|
|
244
|
-
socket.on("error", () => unregisterTuiSession(socket));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
_send(socket, payload) {
|
|
248
|
-
writeNdjson(socket, payload);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
_getSessionKey(socket) {
|
|
252
|
-
return SOCKET_STATE.get(socket)?.sessionKey || null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
_ensureSession(sessionKey, payload = {}) {
|
|
256
|
-
let session = this.stateStore.getChatSession(sessionKey);
|
|
257
|
-
const requestedAgent = resolveRequestedAgent(this.runtime, payload.agent, session?.agent_id || this.defaultAgent);
|
|
258
|
-
|
|
259
|
-
if (!session) {
|
|
260
|
-
this.stateStore.bindChatSession({
|
|
261
|
-
chatId: sessionKey,
|
|
262
|
-
agentId: requestedAgent,
|
|
263
|
-
boundBy: "default",
|
|
264
|
-
});
|
|
265
|
-
session = this.stateStore.getChatSession(sessionKey);
|
|
266
|
-
} else if (payload.agent && session.agent_id !== requestedAgent) {
|
|
267
|
-
this.stateStore.bindChatSession({
|
|
268
|
-
chatId: sessionKey,
|
|
269
|
-
agentId: requestedAgent,
|
|
270
|
-
boundBy: "user",
|
|
271
|
-
});
|
|
272
|
-
session = this.stateStore.getChatSession(sessionKey);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (payload.model !== undefined) {
|
|
276
|
-
this.stateStore.db.prepare("UPDATE chat_sessions SET model_override = ? WHERE chat_id = ?")
|
|
277
|
-
.run(payload.model || null, sessionKey);
|
|
278
|
-
session = this.stateStore.getChatSession(sessionKey);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return session;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async _handlePayload(socket, payload) {
|
|
285
|
-
switch (payload?.type) {
|
|
286
|
-
case "hello":
|
|
287
|
-
await this._handleHello(socket, payload);
|
|
288
|
-
return;
|
|
289
|
-
case "user_message":
|
|
290
|
-
await this._handleText(socket, normalizeText(payload.text), payload);
|
|
291
|
-
return;
|
|
292
|
-
case "slash_command":
|
|
293
|
-
await this._handleText(socket, normalizeText(payload.command || payload.text), { ...payload, forceSlash: true });
|
|
294
|
-
return;
|
|
295
|
-
case "approval":
|
|
296
|
-
await this._handleApproval(socket, payload);
|
|
297
|
-
return;
|
|
298
|
-
case "abort":
|
|
299
|
-
await this._handleAbort(socket);
|
|
300
|
-
return;
|
|
301
|
-
case "ping":
|
|
302
|
-
this._send(socket, { type: "pong" });
|
|
303
|
-
return;
|
|
304
|
-
default:
|
|
305
|
-
this._send(socket, { type: "error", message: `Unsupported message type ${payload?.type || "(missing)"}.`, code: "UNSUPPORTED_TYPE" });
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async _handleHello(socket, payload) {
|
|
310
|
-
const sessionId = normalizeText(payload.sessionId || `${process.pid}-${Date.now()}`);
|
|
311
|
-
const sessionKey = buildTuiSessionKey(sessionId);
|
|
312
|
-
registerTuiSession(sessionKey, socket, { sessionId });
|
|
313
|
-
this._ensureSession(sessionKey, payload);
|
|
314
|
-
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async _handleText(socket, text, payload = {}) {
|
|
318
|
-
const sessionKey = this._getSessionKey(socket);
|
|
319
|
-
if (!sessionKey) {
|
|
320
|
-
this._send(socket, { type: "error", message: "Session not initialized. Send a hello payload first.", code: "SESSION_REQUIRED" });
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const cleanText = text.trim();
|
|
325
|
-
if (!cleanText) {
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const session = this._ensureSession(sessionKey, payload);
|
|
330
|
-
const shouldRouteAsSlash = payload.forceSlash || cleanText.startsWith("/");
|
|
331
|
-
if (shouldRouteAsSlash) {
|
|
332
|
-
const commandText = cleanText.startsWith("/") ? cleanText : `/${cleanText}`;
|
|
333
|
-
const commandResult = await this.commandHandler._handleCommand(commandText, sessionKey);
|
|
334
|
-
if (commandResult) {
|
|
335
|
-
this._send(socket, {
|
|
336
|
-
type: "agent_response",
|
|
337
|
-
text: commandResult.reply,
|
|
338
|
-
command: commandResult.command,
|
|
339
|
-
jobId: null,
|
|
340
|
-
});
|
|
341
|
-
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const available = session?.agent_id && this.runtime?.agents?.[session.agent_id];
|
|
347
|
-
if (!available) {
|
|
348
|
-
this._send(socket, { type: "error", message: `Agent ${session?.agent_id || "unknown"} is not available.`, code: "AGENT_UNAVAILABLE" });
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const jobId = `tui-${Date.now()}-${nextJobCounter++}`;
|
|
353
|
-
this.stateStore.enqueueInteractiveJob({
|
|
354
|
-
jobId,
|
|
355
|
-
agentId: session.agent_id,
|
|
356
|
-
input: cleanText,
|
|
357
|
-
source: SESSION_PREFIX,
|
|
358
|
-
chatId: sessionKey,
|
|
359
|
-
});
|
|
360
|
-
this._send(socket, {
|
|
361
|
-
type: "status",
|
|
362
|
-
...(await this.buildStatusPayload(sessionKey)),
|
|
363
|
-
queued: true,
|
|
364
|
-
jobId,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async _handleApproval(socket, payload) {
|
|
369
|
-
const gate = this.commandHandler.getExecApprovalGate();
|
|
370
|
-
const approvalId = resolveApprovalId(gate, payload);
|
|
371
|
-
const approved = String(payload.action || "approve").toLowerCase() !== "deny";
|
|
372
|
-
const sessionKey = this._getSessionKey(socket);
|
|
373
|
-
if (!approvalId) {
|
|
374
|
-
this._send(socket, {
|
|
375
|
-
type: "agent_response",
|
|
376
|
-
text: "No pending approval found for this request.",
|
|
377
|
-
command: approved ? "exec_approve" : "exec_deny",
|
|
378
|
-
});
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const resolved = gate.resolve(approvalId, approved, SESSION_PREFIX);
|
|
382
|
-
this._send(socket, {
|
|
383
|
-
type: "agent_response",
|
|
384
|
-
text: resolved
|
|
385
|
-
? `${approved ? "Approved" : "Denied"} exec action ${approvalId}.`
|
|
386
|
-
: `No pending approval found with ID ${approvalId}.`,
|
|
387
|
-
command: approved ? "exec_approve" : "exec_deny",
|
|
388
|
-
});
|
|
389
|
-
if (sessionKey) {
|
|
390
|
-
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async _handleAbort(socket) {
|
|
395
|
-
const sessionKey = this._getSessionKey(socket);
|
|
396
|
-
if (!sessionKey) {
|
|
397
|
-
this._send(socket, { type: "error", message: "Session not initialized.", code: "SESSION_REQUIRED" });
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
const result = await this.commandHandler._handleCommand("/stop", sessionKey);
|
|
401
|
-
if (result) {
|
|
402
|
-
this._send(socket, {
|
|
403
|
-
type: "agent_response",
|
|
404
|
-
text: result.reply,
|
|
405
|
-
command: result.command,
|
|
406
|
-
jobId: null,
|
|
407
|
-
});
|
|
408
|
-
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { TelegramInboundHandler, resolveStatusModel } from "./telegram-inbound.js";
|
|
6
|
+
|
|
7
|
+
const SESSION_PREFIX = "tui";
|
|
8
|
+
const SESSION_REGISTRY = new Map();
|
|
9
|
+
const SOCKET_STATE = new WeakMap();
|
|
10
|
+
let nextJobCounter = 1;
|
|
11
|
+
|
|
12
|
+
function safeUnlink(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
fs.unlinkSync(filePath);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error?.code !== "ENOENT") throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeText(value) {
|
|
21
|
+
return typeof value === "string" ? value : String(value ?? "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeNdjson(socket, payload) {
|
|
25
|
+
if (!socket || socket.destroyed || !socket.writable) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
socket.write(`${JSON.stringify(payload)}\n`);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveRequestedAgent(runtime, requestedAgent, fallbackAgent) {
|
|
33
|
+
const configuredAgents = new Set(Object.keys(runtime?.agents || {}));
|
|
34
|
+
if (requestedAgent && configuredAgents.has(requestedAgent)) {
|
|
35
|
+
return requestedAgent;
|
|
36
|
+
}
|
|
37
|
+
if (fallbackAgent && configuredAgents.has(fallbackAgent)) {
|
|
38
|
+
return fallbackAgent;
|
|
39
|
+
}
|
|
40
|
+
const firstConfigured = configuredAgents.values().next().value;
|
|
41
|
+
return firstConfigured || requestedAgent || fallbackAgent || "main";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveApprovalId(gate, payload = {}) {
|
|
45
|
+
if (payload.approvalId) {
|
|
46
|
+
return String(payload.approvalId);
|
|
47
|
+
}
|
|
48
|
+
if (!payload.jobId) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const pending = gate.getPending().find((entry) => entry.jobId === payload.jobId);
|
|
52
|
+
return pending?.approvalId || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveTuiSocketPath(stateRoot) {
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
return "\\\\.\\pipe\\nemoris-daemon";
|
|
58
|
+
}
|
|
59
|
+
return path.join(stateRoot, "daemon.sock");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildTuiSessionKey(sessionId) {
|
|
63
|
+
return `${SESSION_PREFIX}:${sessionId}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function registerTuiSession(sessionKey, socket, meta = {}) {
|
|
67
|
+
const existing = SESSION_REGISTRY.get(sessionKey);
|
|
68
|
+
if (existing?.socket && existing.socket !== socket) {
|
|
69
|
+
existing.socket.destroy();
|
|
70
|
+
}
|
|
71
|
+
SESSION_REGISTRY.set(sessionKey, { socket, meta });
|
|
72
|
+
SOCKET_STATE.set(socket, { sessionKey, ...meta });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function unregisterTuiSession(socket) {
|
|
76
|
+
const state = SOCKET_STATE.get(socket);
|
|
77
|
+
if (!state?.sessionKey) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const current = SESSION_REGISTRY.get(state.sessionKey);
|
|
81
|
+
if (current?.socket === socket) {
|
|
82
|
+
SESSION_REGISTRY.delete(state.sessionKey);
|
|
83
|
+
}
|
|
84
|
+
SOCKET_STATE.delete(socket);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getRegisteredTuiSession(sessionKey) {
|
|
88
|
+
return SESSION_REGISTRY.get(sessionKey) || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function sendTuiEvent(sessionKey, payload) {
|
|
92
|
+
const entry = SESSION_REGISTRY.get(sessionKey);
|
|
93
|
+
if (!entry) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return writeNdjson(entry.socket, payload);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resetTuiSessionRegistry() {
|
|
100
|
+
for (const { socket } of SESSION_REGISTRY.values()) {
|
|
101
|
+
if (socket && !socket.destroyed) {
|
|
102
|
+
socket.destroy();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
SESSION_REGISTRY.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class TuiServer {
|
|
109
|
+
constructor({
|
|
110
|
+
stateStore,
|
|
111
|
+
stateRoot,
|
|
112
|
+
runtime,
|
|
113
|
+
contextLedger = null,
|
|
114
|
+
execApprovalGate = null,
|
|
115
|
+
logger = console,
|
|
116
|
+
} = {}) {
|
|
117
|
+
this.stateStore = stateStore;
|
|
118
|
+
this.stateRoot = stateRoot;
|
|
119
|
+
this.runtime = runtime || {};
|
|
120
|
+
this.contextLedger = contextLedger;
|
|
121
|
+
this.execApprovalGate = execApprovalGate || null;
|
|
122
|
+
this.logger = logger;
|
|
123
|
+
this.server = null;
|
|
124
|
+
this.socketPath = resolveTuiSocketPath(stateRoot);
|
|
125
|
+
this.defaultAgent = resolveRequestedAgent(
|
|
126
|
+
runtime,
|
|
127
|
+
runtime?.runtime?.telegram?.defaultAgent,
|
|
128
|
+
"main",
|
|
129
|
+
);
|
|
130
|
+
const agentNames = Object.fromEntries(
|
|
131
|
+
Object.entries(runtime?.agents || {}).map(([agentId, config]) => [agentId, config?.name || agentId]),
|
|
132
|
+
);
|
|
133
|
+
this.commandHandler = new TelegramInboundHandler({
|
|
134
|
+
stateStore,
|
|
135
|
+
telegramConfig: {
|
|
136
|
+
defaultAgent: this.defaultAgent,
|
|
137
|
+
operatorChatId: "",
|
|
138
|
+
authorizedChatIds: [],
|
|
139
|
+
pollingMode: false,
|
|
140
|
+
sessionLabelPrefix: SESSION_PREFIX,
|
|
141
|
+
},
|
|
142
|
+
availablePeers: Object.keys(runtime?.agents || {}),
|
|
143
|
+
contextLedger,
|
|
144
|
+
agentNames,
|
|
145
|
+
routerConfig: runtime?.router || null,
|
|
146
|
+
agentConfigs: runtime?.agents || null,
|
|
147
|
+
execApprovalGate,
|
|
148
|
+
logger,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setExecApprovalGate(execApprovalGate) {
|
|
153
|
+
this.execApprovalGate = execApprovalGate || null;
|
|
154
|
+
this.commandHandler._execApprovalGate = execApprovalGate || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async start() {
|
|
158
|
+
if (this.server) {
|
|
159
|
+
return { socketPath: this.socketPath };
|
|
160
|
+
}
|
|
161
|
+
if (process.platform !== "win32") {
|
|
162
|
+
safeUnlink(this.socketPath);
|
|
163
|
+
}
|
|
164
|
+
this.server = net.createServer((socket) => this._handleConnection(socket));
|
|
165
|
+
await new Promise((resolve, reject) => {
|
|
166
|
+
const onError = (error) => {
|
|
167
|
+
this.server?.off("listening", onListening);
|
|
168
|
+
reject(error);
|
|
169
|
+
};
|
|
170
|
+
const onListening = () => {
|
|
171
|
+
this.server?.off("error", onError);
|
|
172
|
+
resolve();
|
|
173
|
+
};
|
|
174
|
+
this.server.once("error", onError);
|
|
175
|
+
this.server.once("listening", onListening);
|
|
176
|
+
this.server.listen(this.socketPath);
|
|
177
|
+
});
|
|
178
|
+
return { socketPath: this.socketPath };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async close() {
|
|
182
|
+
if (!this.server) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const server = this.server;
|
|
186
|
+
this.server = null;
|
|
187
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
188
|
+
if (process.platform !== "win32") {
|
|
189
|
+
safeUnlink(this.socketPath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async buildStatusPayload(sessionKey) {
|
|
194
|
+
const session = this.stateStore.getChatSession(sessionKey);
|
|
195
|
+
const usage = this.stateStore.db?.prepare(`
|
|
196
|
+
SELECT
|
|
197
|
+
COALESCE(SUM(tokens_in + tokens_out), 0) as tokens,
|
|
198
|
+
COALESCE(SUM(cost_usd), 0) as cost
|
|
199
|
+
FROM usage_log
|
|
200
|
+
WHERE session_id = ?
|
|
201
|
+
`).get(sessionKey) || { tokens: 0, cost: 0 };
|
|
202
|
+
return {
|
|
203
|
+
type: "status",
|
|
204
|
+
sessionKey,
|
|
205
|
+
agent: session?.agent_id || this.defaultAgent,
|
|
206
|
+
model: resolveStatusModel(session, {
|
|
207
|
+
routerConfig: this.runtime?.router || null,
|
|
208
|
+
agentConfigs: this.runtime?.agents || null,
|
|
209
|
+
}) || "default",
|
|
210
|
+
tokens: usage.tokens || 0,
|
|
211
|
+
cost: usage.cost || 0,
|
|
212
|
+
connected: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_handleConnection(socket) {
|
|
217
|
+
socket.setEncoding("utf8");
|
|
218
|
+
let buffer = "";
|
|
219
|
+
|
|
220
|
+
socket.on("data", async (chunk) => {
|
|
221
|
+
buffer += chunk;
|
|
222
|
+
const lines = buffer.split("\n");
|
|
223
|
+
buffer = lines.pop() || "";
|
|
224
|
+
for (const rawLine of lines) {
|
|
225
|
+
const line = rawLine.trim();
|
|
226
|
+
if (!line) continue;
|
|
227
|
+
let payload;
|
|
228
|
+
try {
|
|
229
|
+
payload = JSON.parse(line);
|
|
230
|
+
} catch {
|
|
231
|
+
this._send(socket, { type: "error", message: "Invalid JSON payload.", code: "INVALID_JSON" });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
await this._handlePayload(socket, payload);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this.logger.error?.(`[tui-server] ${error.message}`);
|
|
238
|
+
this._send(socket, { type: "error", message: error.message, code: "TUI_SERVER_ERROR" });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
socket.on("close", () => unregisterTuiSession(socket));
|
|
244
|
+
socket.on("error", () => unregisterTuiSession(socket));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_send(socket, payload) {
|
|
248
|
+
writeNdjson(socket, payload);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_getSessionKey(socket) {
|
|
252
|
+
return SOCKET_STATE.get(socket)?.sessionKey || null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_ensureSession(sessionKey, payload = {}) {
|
|
256
|
+
let session = this.stateStore.getChatSession(sessionKey);
|
|
257
|
+
const requestedAgent = resolveRequestedAgent(this.runtime, payload.agent, session?.agent_id || this.defaultAgent);
|
|
258
|
+
|
|
259
|
+
if (!session) {
|
|
260
|
+
this.stateStore.bindChatSession({
|
|
261
|
+
chatId: sessionKey,
|
|
262
|
+
agentId: requestedAgent,
|
|
263
|
+
boundBy: "default",
|
|
264
|
+
});
|
|
265
|
+
session = this.stateStore.getChatSession(sessionKey);
|
|
266
|
+
} else if (payload.agent && session.agent_id !== requestedAgent) {
|
|
267
|
+
this.stateStore.bindChatSession({
|
|
268
|
+
chatId: sessionKey,
|
|
269
|
+
agentId: requestedAgent,
|
|
270
|
+
boundBy: "user",
|
|
271
|
+
});
|
|
272
|
+
session = this.stateStore.getChatSession(sessionKey);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (payload.model !== undefined) {
|
|
276
|
+
this.stateStore.db.prepare("UPDATE chat_sessions SET model_override = ? WHERE chat_id = ?")
|
|
277
|
+
.run(payload.model || null, sessionKey);
|
|
278
|
+
session = this.stateStore.getChatSession(sessionKey);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return session;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async _handlePayload(socket, payload) {
|
|
285
|
+
switch (payload?.type) {
|
|
286
|
+
case "hello":
|
|
287
|
+
await this._handleHello(socket, payload);
|
|
288
|
+
return;
|
|
289
|
+
case "user_message":
|
|
290
|
+
await this._handleText(socket, normalizeText(payload.text), payload);
|
|
291
|
+
return;
|
|
292
|
+
case "slash_command":
|
|
293
|
+
await this._handleText(socket, normalizeText(payload.command || payload.text), { ...payload, forceSlash: true });
|
|
294
|
+
return;
|
|
295
|
+
case "approval":
|
|
296
|
+
await this._handleApproval(socket, payload);
|
|
297
|
+
return;
|
|
298
|
+
case "abort":
|
|
299
|
+
await this._handleAbort(socket);
|
|
300
|
+
return;
|
|
301
|
+
case "ping":
|
|
302
|
+
this._send(socket, { type: "pong" });
|
|
303
|
+
return;
|
|
304
|
+
default:
|
|
305
|
+
this._send(socket, { type: "error", message: `Unsupported message type ${payload?.type || "(missing)"}.`, code: "UNSUPPORTED_TYPE" });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async _handleHello(socket, payload) {
|
|
310
|
+
const sessionId = normalizeText(payload.sessionId || `${process.pid}-${Date.now()}`);
|
|
311
|
+
const sessionKey = buildTuiSessionKey(sessionId);
|
|
312
|
+
registerTuiSession(sessionKey, socket, { sessionId });
|
|
313
|
+
this._ensureSession(sessionKey, payload);
|
|
314
|
+
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async _handleText(socket, text, payload = {}) {
|
|
318
|
+
const sessionKey = this._getSessionKey(socket);
|
|
319
|
+
if (!sessionKey) {
|
|
320
|
+
this._send(socket, { type: "error", message: "Session not initialized. Send a hello payload first.", code: "SESSION_REQUIRED" });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const cleanText = text.trim();
|
|
325
|
+
if (!cleanText) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const session = this._ensureSession(sessionKey, payload);
|
|
330
|
+
const shouldRouteAsSlash = payload.forceSlash || cleanText.startsWith("/");
|
|
331
|
+
if (shouldRouteAsSlash) {
|
|
332
|
+
const commandText = cleanText.startsWith("/") ? cleanText : `/${cleanText}`;
|
|
333
|
+
const commandResult = await this.commandHandler._handleCommand(commandText, sessionKey);
|
|
334
|
+
if (commandResult) {
|
|
335
|
+
this._send(socket, {
|
|
336
|
+
type: "agent_response",
|
|
337
|
+
text: commandResult.reply,
|
|
338
|
+
command: commandResult.command,
|
|
339
|
+
jobId: null,
|
|
340
|
+
});
|
|
341
|
+
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const available = session?.agent_id && this.runtime?.agents?.[session.agent_id];
|
|
347
|
+
if (!available) {
|
|
348
|
+
this._send(socket, { type: "error", message: `Agent ${session?.agent_id || "unknown"} is not available.`, code: "AGENT_UNAVAILABLE" });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const jobId = `tui-${Date.now()}-${nextJobCounter++}`;
|
|
353
|
+
this.stateStore.enqueueInteractiveJob({
|
|
354
|
+
jobId,
|
|
355
|
+
agentId: session.agent_id,
|
|
356
|
+
input: cleanText,
|
|
357
|
+
source: SESSION_PREFIX,
|
|
358
|
+
chatId: sessionKey,
|
|
359
|
+
});
|
|
360
|
+
this._send(socket, {
|
|
361
|
+
type: "status",
|
|
362
|
+
...(await this.buildStatusPayload(sessionKey)),
|
|
363
|
+
queued: true,
|
|
364
|
+
jobId,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async _handleApproval(socket, payload) {
|
|
369
|
+
const gate = this.commandHandler.getExecApprovalGate();
|
|
370
|
+
const approvalId = resolveApprovalId(gate, payload);
|
|
371
|
+
const approved = String(payload.action || "approve").toLowerCase() !== "deny";
|
|
372
|
+
const sessionKey = this._getSessionKey(socket);
|
|
373
|
+
if (!approvalId) {
|
|
374
|
+
this._send(socket, {
|
|
375
|
+
type: "agent_response",
|
|
376
|
+
text: "No pending approval found for this request.",
|
|
377
|
+
command: approved ? "exec_approve" : "exec_deny",
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const resolved = gate.resolve(approvalId, approved, SESSION_PREFIX);
|
|
382
|
+
this._send(socket, {
|
|
383
|
+
type: "agent_response",
|
|
384
|
+
text: resolved
|
|
385
|
+
? `${approved ? "Approved" : "Denied"} exec action ${approvalId}.`
|
|
386
|
+
: `No pending approval found with ID ${approvalId}.`,
|
|
387
|
+
command: approved ? "exec_approve" : "exec_deny",
|
|
388
|
+
});
|
|
389
|
+
if (sessionKey) {
|
|
390
|
+
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async _handleAbort(socket) {
|
|
395
|
+
const sessionKey = this._getSessionKey(socket);
|
|
396
|
+
if (!sessionKey) {
|
|
397
|
+
this._send(socket, { type: "error", message: "Session not initialized.", code: "SESSION_REQUIRED" });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const result = await this.commandHandler._handleCommand("/stop", sessionKey);
|
|
401
|
+
if (result) {
|
|
402
|
+
this._send(socket, {
|
|
403
|
+
type: "agent_response",
|
|
404
|
+
text: result.reply,
|
|
405
|
+
command: result.command,
|
|
406
|
+
jobId: null,
|
|
407
|
+
});
|
|
408
|
+
this._send(socket, await this.buildStatusPayload(sessionKey));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|