chainlesschain 0.45.12 → 0.45.20

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.
Files changed (78) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AppLayout-BfLjLMsm.js → AppLayout-B00RARl2.js} +1 -1
  3. package/src/assets/web-panel/assets/{Chat-DP7PO9Li.js → Chat-DXtvKoM0.js} +1 -1
  4. package/src/assets/web-panel/assets/{Cron-DyQF-7R1.js → Cron-BJ4ODHOy.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  6. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  7. package/src/assets/web-panel/assets/{Logs-BOii-AoO.js → Logs-CSeKZEG_.js} +1 -1
  8. package/src/assets/web-panel/assets/{McpTools-DmiJtJYr.js → McpTools-BYQAK11r.js} +1 -1
  9. package/src/assets/web-panel/assets/{Memory-CDRMMobU.js → Memory-gkUAPyuZ.js} +1 -1
  10. package/src/assets/web-panel/assets/{Notes-CVhqqoS1.js → Notes-bjNrQgAo.js} +1 -1
  11. package/src/assets/web-panel/assets/{Providers-Dkt7021l.js → Providers-Dbf57Tbv.js} +1 -1
  12. package/src/assets/web-panel/assets/{Services-DUDL_UGb.js → Services-CS0oMdxh.js} +1 -1
  13. package/src/assets/web-panel/assets/{Skills-DXXELJc3.js → Skills-B2fgruv8.js} +1 -1
  14. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  15. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  16. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  17. package/src/assets/web-panel/assets/{index-vW799KpE.js → index-CF2CqPYX.js} +2 -2
  18. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  19. package/src/assets/web-panel/index.html +1 -1
  20. package/src/commands/agent.js +7 -8
  21. package/src/commands/chat.js +9 -11
  22. package/src/commands/serve.js +11 -106
  23. package/src/commands/session.js +101 -0
  24. package/src/commands/ui.js +10 -151
  25. package/src/gateways/repl/agent-repl.js +1 -0
  26. package/src/gateways/repl/chat-repl.js +1 -0
  27. package/src/gateways/ui/web-ui-server.js +1 -0
  28. package/src/gateways/ws/action-protocol.js +83 -0
  29. package/src/gateways/ws/message-dispatcher.js +73 -0
  30. package/src/gateways/ws/session-protocol.js +396 -0
  31. package/src/gateways/ws/task-protocol.js +55 -0
  32. package/src/gateways/ws/worktree-protocol.js +315 -0
  33. package/src/gateways/ws/ws-server.js +4 -0
  34. package/src/gateways/ws/ws-session-gateway.js +1 -0
  35. package/src/harness/background-task-manager.js +506 -0
  36. package/src/harness/background-task-worker.js +48 -0
  37. package/src/harness/compression-telemetry.js +214 -0
  38. package/src/harness/feature-flags.js +157 -0
  39. package/src/harness/jsonl-session-store.js +452 -0
  40. package/src/harness/prompt-compressor.js +416 -0
  41. package/src/harness/worktree-isolator.js +845 -0
  42. package/src/lib/agent-core.js +246 -45
  43. package/src/lib/background-task-manager.js +1 -305
  44. package/src/lib/background-task-worker.js +1 -50
  45. package/src/lib/compression-telemetry.js +5 -0
  46. package/src/lib/feature-flags.js +7 -182
  47. package/src/lib/interaction-adapter.js +32 -6
  48. package/src/lib/jsonl-session-store.js +21 -237
  49. package/src/lib/prompt-compressor.js +10 -481
  50. package/src/lib/sub-agent-context.js +21 -1
  51. package/src/lib/worktree-isolator.js +13 -231
  52. package/src/lib/ws-agent-handler.js +1 -0
  53. package/src/lib/ws-server.js +138 -387
  54. package/src/lib/ws-session-manager.js +82 -1
  55. package/src/repl/agent-repl.js +11 -0
  56. package/src/runtime/agent-runtime.js +417 -0
  57. package/src/runtime/contracts/agent-turn.js +11 -0
  58. package/src/runtime/contracts/session-record.js +31 -0
  59. package/src/runtime/contracts/task-record.js +18 -0
  60. package/src/runtime/contracts/telemetry-record.js +23 -0
  61. package/src/runtime/contracts/worktree-record.js +14 -0
  62. package/src/runtime/index.js +13 -0
  63. package/src/runtime/policies/agent-policy.js +45 -0
  64. package/src/runtime/runtime-context.js +14 -0
  65. package/src/runtime/runtime-events.js +37 -0
  66. package/src/runtime/runtime-factory.js +50 -0
  67. package/src/tools/index.js +22 -0
  68. package/src/tools/legacy-agent-tools.js +171 -0
  69. package/src/tools/registry.js +141 -0
  70. package/src/tools/tool-context.js +28 -0
  71. package/src/tools/tool-permissions.js +28 -0
  72. package/src/tools/tool-telemetry.js +39 -0
  73. package/src/assets/web-panel/assets/Dashboard-BGGdnr6t.js +0 -3
  74. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  75. package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +0 -1
  76. package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +0 -1
  77. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  78. package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
@@ -20,6 +20,8 @@ import {
20
20
  } from "./session-manager.js";
21
21
  import { buildSystemPrompt } from "./agent-core.js";
22
22
  import { SubAgentRegistry } from "./sub-agent-registry.js";
23
+ import { createWorktree, removeWorktree } from "./worktree-isolator.js";
24
+ import { isGitRepo } from "./git-integration.js";
23
25
 
24
26
  /**
25
27
  * @typedef {object} Session
@@ -32,7 +34,11 @@ import { SubAgentRegistry } from "./sub-agent-registry.js";
32
34
  * @property {string|null} apiKey
33
35
  * @property {string|null} baseUrl
34
36
  * @property {string} projectRoot
37
+ * @property {string} baseProjectRoot
35
38
  * @property {string|null} rulesContent
39
+ * @property {object|null} hostManagedToolPolicy
40
+ * @property {boolean} worktreeIsolation
41
+ * @property {object|null} worktree
36
42
  * @property {PlanModeManager} planManager
37
43
  * @property {CLIContextEngineering|null} contextEngine
38
44
  * @property {CLIPermanentMemory|null} permanentMemory
@@ -78,12 +84,13 @@ export class WSSessionManager {
78
84
  * @param {string} [options.model]
79
85
  * @param {string} [options.apiKey]
80
86
  * @param {string} [options.baseUrl]
87
+ * @param {object} [options.hostManagedToolPolicy]
81
88
  * @returns {{ sessionId: string }}
82
89
  */
83
90
  createSession(options = {}) {
84
91
  const sessionId = this._generateId();
85
92
  const type = options.type || "agent";
86
- const projectRoot = options.projectRoot || this.defaultProjectRoot;
93
+ const baseProjectRoot = options.projectRoot || this.defaultProjectRoot;
87
94
  const cfgLlm = this.config?.llm || {};
88
95
  const provider = options.provider || cfgLlm.provider || "ollama";
89
96
  const model =
@@ -93,6 +100,16 @@ export class WSSessionManager {
93
100
  const baseUrl =
94
101
  options.baseUrl || cfgLlm.baseUrl || "http://localhost:11434";
95
102
  const apiKey = options.apiKey || cfgLlm.apiKey || null;
103
+ const worktreeIsolationRequested = options.worktreeIsolation === true;
104
+ const isolatedWorkspace = this._prepareSessionWorkspace(
105
+ baseProjectRoot,
106
+ sessionId,
107
+ {
108
+ worktreeIsolation: worktreeIsolationRequested,
109
+ },
110
+ );
111
+ const projectRoot = isolatedWorkspace.projectRoot;
112
+ const worktree = isolatedWorkspace.worktree;
96
113
 
97
114
  // Project context (rules.md, persona) is now loaded by buildSystemPrompt()
98
115
 
@@ -151,8 +168,12 @@ export class WSSessionManager {
151
168
  model,
152
169
  apiKey,
153
170
  baseUrl,
171
+ hostManagedToolPolicy: options.hostManagedToolPolicy || null,
154
172
  projectRoot,
173
+ baseProjectRoot,
155
174
  rulesContent: null,
175
+ worktreeIsolation: worktreeIsolationRequested,
176
+ worktree,
156
177
  planManager,
157
178
  contextEngine,
158
179
  permanentMemory,
@@ -226,8 +247,12 @@ export class WSSessionManager {
226
247
  model: dbSession.model || null,
227
248
  apiKey: null,
228
249
  baseUrl: "http://localhost:11434",
250
+ hostManagedToolPolicy: null,
229
251
  projectRoot: this.defaultProjectRoot,
252
+ baseProjectRoot: this.defaultProjectRoot,
230
253
  rulesContent: null,
254
+ worktreeIsolation: false,
255
+ worktree: null,
231
256
  planManager,
232
257
  contextEngine,
233
258
  permanentMemory,
@@ -284,6 +309,16 @@ export class WSSessionManager {
284
309
  session.planManager.removeAllListeners();
285
310
  }
286
311
 
312
+ if (session.worktree?.path && session.baseProjectRoot) {
313
+ try {
314
+ removeWorktree(session.baseProjectRoot, session.worktree.path, {
315
+ deleteBranch: true,
316
+ });
317
+ } catch (_err) {
318
+ // Best-effort cleanup.
319
+ }
320
+ }
321
+
287
322
  this.sessions.delete(sessionId);
288
323
  }
289
324
 
@@ -304,6 +339,9 @@ export class WSSessionManager {
304
339
  provider: session.provider,
305
340
  model: session.model,
306
341
  messageCount: session.messages.length,
342
+ baseProjectRoot: session.baseProjectRoot,
343
+ worktreeIsolation: session.worktreeIsolation === true,
344
+ worktree: session.worktree || null,
307
345
  createdAt: session.createdAt,
308
346
  lastActivity: session.lastActivity,
309
347
  });
@@ -346,6 +384,22 @@ export class WSSessionManager {
346
384
  return this.sessions.get(sessionId) || null;
347
385
  }
348
386
 
387
+ /**
388
+ * Update host-managed tool policy for an active session.
389
+ *
390
+ * @param {string} sessionId
391
+ * @param {object|null} hostManagedToolPolicy
392
+ * @returns {Session|null}
393
+ */
394
+ updateSessionPolicy(sessionId, hostManagedToolPolicy) {
395
+ const session = this.sessions.get(sessionId);
396
+ if (!session) return null;
397
+
398
+ session.hostManagedToolPolicy = hostManagedToolPolicy || null;
399
+ session.lastActivity = new Date().toISOString();
400
+ return session;
401
+ }
402
+
349
403
  /**
350
404
  * Persist current messages for a session.
351
405
  */
@@ -361,4 +415,31 @@ export class WSSessionManager {
361
415
 
362
416
  session.lastActivity = new Date().toISOString();
363
417
  }
418
+
419
+ _prepareSessionWorkspace(projectRoot, sessionId, options = {}) {
420
+ if (options.worktreeIsolation !== true) {
421
+ return {
422
+ projectRoot,
423
+ worktree: null,
424
+ };
425
+ }
426
+
427
+ if (!isGitRepo(projectRoot)) {
428
+ throw new Error(
429
+ `Worktree isolation requires a git repository: ${projectRoot}`,
430
+ );
431
+ }
432
+
433
+ const branchName = `coding-agent/${sessionId}`;
434
+ const worktree = createWorktree(projectRoot, branchName);
435
+
436
+ return {
437
+ projectRoot: worktree.path,
438
+ worktree: {
439
+ branch: worktree.branch,
440
+ path: worktree.path,
441
+ baseProjectRoot: projectRoot,
442
+ },
443
+ };
444
+ }
364
445
  }
@@ -48,6 +48,7 @@ import { CLIPermanentMemory } from "../lib/permanent-memory.js";
48
48
  import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
49
49
  import { PromptCompressor } from "../lib/prompt-compressor.js";
50
50
  import { feature } from "../lib/feature-flags.js";
51
+ import { recordCompressionMetric } from "../lib/compression-telemetry.js";
51
52
  import {
52
53
  AGENT_TOOLS,
53
54
  buildSystemPrompt,
@@ -426,6 +427,11 @@ export async function startAgentRepl(options = {}) {
426
427
  await _compressor.compress(messages);
427
428
  messages.length = 0;
428
429
  messages.push(...compacted);
430
+ recordCompressionMetric(stats, {
431
+ source: "manual-compact",
432
+ provider,
433
+ model,
434
+ });
429
435
  logger.info(
430
436
  `Compacted: ${stats.originalMessages} → ${stats.compressedMessages} messages, saved ${stats.saved} tokens (${stats.strategy})`,
431
437
  );
@@ -1126,6 +1132,11 @@ export async function startAgentRepl(options = {}) {
1126
1132
  await _compressor.compress(messages);
1127
1133
  messages.length = 0;
1128
1134
  messages.push(...compacted);
1135
+ recordCompressionMetric(stats, {
1136
+ source: "auto-compact",
1137
+ provider,
1138
+ model: activeModel,
1139
+ });
1129
1140
  if (stats.saved > 0) {
1130
1141
  logger.verbose(
1131
1142
  `Auto-compacted: ${stats.strategy} (saved ${stats.saved} tokens)`,
@@ -0,0 +1,417 @@
1
+ import chalk from "chalk";
2
+ import { execSync } from "child_process";
3
+ import path from "path";
4
+ import {
5
+ RuntimeEventEmitter,
6
+ RUNTIME_EVENTS,
7
+ createRuntimeEvent,
8
+ } from "./runtime-events.js";
9
+ import { createRuntimeContext } from "./runtime-context.js";
10
+ import { createAgentTurnRecord } from "./contracts/agent-turn.js";
11
+ import { logger } from "../lib/logger.js";
12
+ import { bootstrap } from "./bootstrap.js";
13
+ import { startAgentRepl } from "../gateways/repl/agent-repl.js";
14
+ import { startChatRepl } from "../gateways/repl/chat-repl.js";
15
+ import { ChainlessChainWSServer } from "../gateways/ws/ws-server.js";
16
+ import { WSSessionManager } from "../gateways/ws/ws-session-gateway.js";
17
+ import { createWebUIServer } from "../gateways/ui/web-ui-server.js";
18
+ import {
19
+ findProjectRoot,
20
+ loadProjectConfig,
21
+ } from "../lib/project-detector.js";
22
+ import { loadConfig } from "../lib/config-manager.js";
23
+
24
+ function openBrowser(url) {
25
+ try {
26
+ const platform = process.platform;
27
+ if (platform === "win32") {
28
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
29
+ } else if (platform === "darwin") {
30
+ execSync(`open "${url}"`, { stdio: "ignore" });
31
+ } else {
32
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
33
+ }
34
+ } catch (_err) {
35
+ // Non-critical.
36
+ }
37
+ }
38
+
39
+ export class AgentRuntime {
40
+ constructor({ kind, policy, config = null, deps = {} } = {}) {
41
+ this.kind = kind;
42
+ this.policy = policy;
43
+ this.config = config;
44
+ this.context = createRuntimeContext({ kind, policy, config });
45
+ this.events = deps.events || new RuntimeEventEmitter();
46
+ this.deps = {
47
+ startAgentRepl: deps.startAgentRepl || startAgentRepl,
48
+ startChatRepl: deps.startChatRepl || startChatRepl,
49
+ bootstrap: deps.bootstrap || bootstrap,
50
+ createServer:
51
+ deps.createServer ||
52
+ ((options) => new ChainlessChainWSServer(options)),
53
+ createSessionManager:
54
+ deps.createSessionManager ||
55
+ ((options) => new WSSessionManager(options)),
56
+ createWebServer:
57
+ deps.createWebServer || ((options) => createWebUIServer(options)),
58
+ findProjectRoot: deps.findProjectRoot || findProjectRoot,
59
+ loadProjectConfig: deps.loadProjectConfig || loadProjectConfig,
60
+ loadConfig: deps.loadConfig || loadConfig,
61
+ openBrowser: deps.openBrowser || openBrowser,
62
+ runTurn: deps.runTurn || null,
63
+ logger: deps.logger || logger,
64
+ };
65
+ }
66
+
67
+ on(eventName, listener) {
68
+ this.events.on(eventName, listener);
69
+ return this;
70
+ }
71
+
72
+ emit(eventName, payload) {
73
+ const event = createRuntimeEvent(eventName, payload, {
74
+ kind: this.kind,
75
+ sessionId: this.policy?.sessionId || null,
76
+ });
77
+ this.events.emit(eventName, event);
78
+ }
79
+
80
+ async resumeSession(sessionId) {
81
+ const nextSessionId = sessionId || this.policy.sessionId;
82
+ if (!nextSessionId) {
83
+ throw new Error("resumeSession requires a sessionId.");
84
+ }
85
+
86
+ this.policy = {
87
+ ...this.policy,
88
+ sessionId: nextSessionId,
89
+ };
90
+ this.context = createRuntimeContext({
91
+ kind: this.kind,
92
+ policy: this.policy,
93
+ config: this.config,
94
+ });
95
+
96
+ this.emit(RUNTIME_EVENTS.SESSION_RESUME, {
97
+ kind: this.kind,
98
+ sessionId: nextSessionId,
99
+ });
100
+
101
+ if (this.kind === "chat") {
102
+ return this.startChatSession();
103
+ }
104
+ if (this.kind === "agent") {
105
+ return this.startAgentSession();
106
+ }
107
+
108
+ throw new Error(`resumeSession is not supported for runtime kind "${this.kind}".`);
109
+ }
110
+
111
+ async runTurn(input, meta = {}) {
112
+ if (typeof this.deps.runTurn !== "function") {
113
+ throw new Error(`runTurn is not configured for runtime kind "${this.kind}".`);
114
+ }
115
+
116
+ const startedAt = Date.now();
117
+ this.emit(RUNTIME_EVENTS.TURN_START, createAgentTurnRecord({
118
+ kind: this.kind,
119
+ input,
120
+ meta,
121
+ sessionId: this.policy.sessionId || null,
122
+ startedAt,
123
+ }));
124
+
125
+ const result = await this.deps.runTurn({
126
+ input,
127
+ meta,
128
+ kind: this.kind,
129
+ policy: this.policy,
130
+ context: this.context,
131
+ });
132
+
133
+ this.emit(RUNTIME_EVENTS.TURN_END, createAgentTurnRecord({
134
+ kind: this.kind,
135
+ input,
136
+ meta,
137
+ result,
138
+ sessionId: this.policy.sessionId || null,
139
+ startedAt,
140
+ endedAt: Date.now(),
141
+ }));
142
+
143
+ return result;
144
+ }
145
+
146
+ async startAgentSession() {
147
+ this.emit(RUNTIME_EVENTS.RUNTIME_START, {
148
+ kind: this.kind,
149
+ policy: this.policy,
150
+ });
151
+ this.emit(RUNTIME_EVENTS.SESSION_START, {
152
+ kind: this.kind,
153
+ sessionId: this.policy.sessionId || null,
154
+ });
155
+ return this.deps.startAgentRepl(this.policy);
156
+ }
157
+
158
+ async startChatSession() {
159
+ this.emit(RUNTIME_EVENTS.RUNTIME_START, {
160
+ kind: this.kind,
161
+ policy: this.policy,
162
+ });
163
+ this.emit(RUNTIME_EVENTS.SESSION_START, {
164
+ kind: this.kind,
165
+ sessionId: this.policy.sessionId || null,
166
+ });
167
+ return this.deps.startChatRepl(this.policy);
168
+ }
169
+
170
+ async startServer() {
171
+ const { logger: runtimeLogger } = this.deps;
172
+ const {
173
+ port,
174
+ maxConnections,
175
+ timeout,
176
+ token,
177
+ allowRemote,
178
+ project,
179
+ } = this.policy;
180
+ let { host } = this.policy;
181
+
182
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
183
+ throw new Error("Invalid port number. Must be between 1 and 65535.");
184
+ }
185
+
186
+ if (allowRemote) {
187
+ if (!token) {
188
+ throw new Error("--allow-remote requires --token for security.");
189
+ }
190
+ host = "0.0.0.0";
191
+ }
192
+
193
+ let db = null;
194
+ try {
195
+ const ctx = await this.deps.bootstrap({ skipDb: false });
196
+ db = ctx.db?.getDb?.() || null;
197
+ } catch (_err) {
198
+ runtimeLogger.log(
199
+ chalk.yellow(
200
+ " Warning: Database not available, sessions will be in-memory only",
201
+ ),
202
+ );
203
+ }
204
+
205
+ const sessionManager = this.deps.createSessionManager({
206
+ db,
207
+ defaultProjectRoot: project,
208
+ });
209
+
210
+ const server = this.deps.createServer({
211
+ port,
212
+ host,
213
+ token,
214
+ maxConnections,
215
+ timeout,
216
+ sessionManager,
217
+ });
218
+
219
+ server.on("connection", ({ clientId, ip }) => {
220
+ runtimeLogger.log(chalk.green(` + Client connected: ${clientId} (${ip})`));
221
+ });
222
+
223
+ server.on("disconnection", ({ clientId, reason }) => {
224
+ const extra = reason ? ` (${reason})` : "";
225
+ runtimeLogger.log(
226
+ chalk.yellow(` - Client disconnected: ${clientId}${extra}`),
227
+ );
228
+ });
229
+
230
+ server.on("command:start", ({ id, command }) => {
231
+ runtimeLogger.log(chalk.cyan(` > [${id}] ${command}`));
232
+ });
233
+
234
+ server.on("command:end", ({ id, exitCode }) => {
235
+ const color = exitCode === 0 ? chalk.green : chalk.red;
236
+ runtimeLogger.log(color(` < [${id}] exit ${exitCode}`));
237
+ });
238
+
239
+ server.on("session:create", ({ sessionId, type }) => {
240
+ runtimeLogger.log(
241
+ chalk.green(` + Session created: ${sessionId} (${type})`),
242
+ );
243
+ });
244
+
245
+ server.on("session:close", ({ sessionId }) => {
246
+ runtimeLogger.log(chalk.yellow(` - Session closed: ${sessionId}`));
247
+ });
248
+
249
+ const shutdownHandler = async () => {
250
+ runtimeLogger.log(
251
+ "\n" + chalk.yellow("Shutting down WebSocket server..."),
252
+ );
253
+ await server.stop();
254
+ process.exit(0);
255
+ };
256
+
257
+ process.on("SIGINT", shutdownHandler);
258
+ process.on("SIGTERM", shutdownHandler);
259
+
260
+ await server.start();
261
+
262
+ this.emit(RUNTIME_EVENTS.RUNTIME_START, {
263
+ kind: this.kind,
264
+ policy: { ...this.policy, host },
265
+ });
266
+ this.emit(RUNTIME_EVENTS.SERVER_START, {
267
+ host,
268
+ port,
269
+ project,
270
+ });
271
+
272
+ runtimeLogger.log("");
273
+ runtimeLogger.log(chalk.bold(" ChainlessChain WebSocket Server"));
274
+ runtimeLogger.log("");
275
+ runtimeLogger.log(` Address: ${chalk.cyan(`ws://${host}:${port}`)}`);
276
+ runtimeLogger.log(
277
+ ` Auth: ${token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
278
+ );
279
+ runtimeLogger.log(` Sessions: ${chalk.green("enabled")}`);
280
+ runtimeLogger.log(` Project: ${project}`);
281
+ runtimeLogger.log(` Max conn: ${maxConnections}`);
282
+ runtimeLogger.log(` Timeout: ${timeout}ms`);
283
+ runtimeLogger.log("");
284
+ runtimeLogger.log(chalk.dim(" Press Ctrl+C to stop"));
285
+ runtimeLogger.log("");
286
+
287
+ return server;
288
+ }
289
+
290
+ async startUiServer() {
291
+ const { logger: runtimeLogger } = this.deps;
292
+ const httpPort = this.policy.port;
293
+ const wsPort = this.policy.wsPort;
294
+ const host = this.policy.host;
295
+
296
+ if (Number.isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
297
+ throw new Error("Invalid --port. Must be between 1 and 65535.");
298
+ }
299
+ if (Number.isNaN(wsPort) || wsPort < 1 || wsPort > 65535) {
300
+ throw new Error("Invalid --ws-port. Must be between 1 and 65535.");
301
+ }
302
+
303
+ const projectRoot = this.deps.findProjectRoot(process.cwd());
304
+ const projectConfig = projectRoot
305
+ ? this.deps.loadProjectConfig(projectRoot)
306
+ : null;
307
+ const projectName =
308
+ projectConfig?.name ||
309
+ (projectRoot ? path.basename(projectRoot) : null);
310
+ const mode = projectRoot ? "project" : "global";
311
+
312
+ let db = null;
313
+ try {
314
+ const ctx = await this.deps.bootstrap({ skipDb: false });
315
+ db = ctx.db?.getDb?.() || null;
316
+ } catch (_err) {
317
+ runtimeLogger.log(
318
+ chalk.yellow(
319
+ " Warning: Database not available, sessions will be in-memory only",
320
+ ),
321
+ );
322
+ }
323
+
324
+ const appConfig = this.deps.loadConfig();
325
+ const sessionManager = this.deps.createSessionManager({
326
+ db,
327
+ defaultProjectRoot: projectRoot || process.cwd(),
328
+ config: appConfig,
329
+ });
330
+
331
+ const wsServer = this.deps.createServer({
332
+ port: wsPort,
333
+ host,
334
+ token: this.policy.token,
335
+ maxConnections: 20,
336
+ timeout: 60000,
337
+ sessionManager,
338
+ });
339
+ await wsServer.start();
340
+
341
+ const httpServer = this.deps.createWebServer({
342
+ wsPort,
343
+ wsToken: this.policy.token,
344
+ wsHost: host === "0.0.0.0" ? "127.0.0.1" : host,
345
+ projectRoot,
346
+ projectName,
347
+ mode,
348
+ staticDir: this.policy.webPanelDir,
349
+ });
350
+
351
+ await new Promise((resolve, reject) => {
352
+ httpServer.listen(httpPort, host, () => resolve());
353
+ httpServer.on("error", reject);
354
+ });
355
+
356
+ const uiUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${httpPort}`;
357
+
358
+ this.emit(RUNTIME_EVENTS.RUNTIME_START, {
359
+ kind: this.kind,
360
+ policy: this.policy,
361
+ });
362
+ this.emit(RUNTIME_EVENTS.SERVER_START, {
363
+ host,
364
+ port: httpPort,
365
+ wsPort,
366
+ mode,
367
+ projectRoot,
368
+ });
369
+
370
+ runtimeLogger.log("");
371
+ runtimeLogger.log(chalk.bold(" ChainlessChain 管理面板"));
372
+ runtimeLogger.log("");
373
+ if (mode === "project") {
374
+ runtimeLogger.log(
375
+ ` Mode: ${chalk.cyan("project")} ${chalk.dim(projectRoot)}`,
376
+ );
377
+ if (projectName) {
378
+ runtimeLogger.log(` Project: ${chalk.green(projectName)}`);
379
+ }
380
+ } else {
381
+ runtimeLogger.log(` Mode: ${chalk.cyan("global")}`);
382
+ }
383
+ runtimeLogger.log(` UI: ${chalk.cyan(uiUrl)}`);
384
+ runtimeLogger.log(` WS: ${chalk.dim(`ws://${host}:${wsPort}`)}`);
385
+ runtimeLogger.log(
386
+ ` Auth: ${this.policy.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
387
+ );
388
+ runtimeLogger.log("");
389
+ runtimeLogger.log(chalk.dim(" Press Ctrl+C to stop"));
390
+ runtimeLogger.log("");
391
+
392
+ if (this.policy.open) {
393
+ this.deps.openBrowser(uiUrl);
394
+ }
395
+
396
+ const shutdown = async () => {
397
+ runtimeLogger.log("\n" + chalk.yellow("Shutting down UI server..."));
398
+ await Promise.all([
399
+ new Promise((resolve) => httpServer.close(resolve)),
400
+ wsServer.stop(),
401
+ ]);
402
+ process.exit(0);
403
+ };
404
+
405
+ process.on("SIGINT", shutdown);
406
+ process.on("SIGTERM", shutdown);
407
+
408
+ return {
409
+ wsServer,
410
+ httpServer,
411
+ uiUrl,
412
+ mode,
413
+ projectRoot,
414
+ projectName,
415
+ };
416
+ }
417
+ }
@@ -0,0 +1,11 @@
1
+ export function createAgentTurnRecord(turn = {}) {
2
+ return {
3
+ kind: turn.kind || null,
4
+ sessionId: turn.sessionId || null,
5
+ input: turn.input,
6
+ meta: turn.meta || {},
7
+ result: turn.result,
8
+ startedAt: turn.startedAt || null,
9
+ endedAt: turn.endedAt || null,
10
+ };
11
+ }
@@ -0,0 +1,31 @@
1
+ import { createWorktreeRecord } from "./worktree-record.js";
2
+
3
+ export function createSessionRecord(session = {}, extras = {}) {
4
+ const history = Array.isArray(extras.history)
5
+ ? extras.history
6
+ : Array.isArray(session.messages)
7
+ ? session.messages.filter((item) => item.role !== "system")
8
+ : [];
9
+
10
+ return {
11
+ id: session.id || extras.sessionId || null,
12
+ type: session.type || extras.sessionType || null,
13
+ provider: session.provider || extras.provider || null,
14
+ model: session.model || extras.model || null,
15
+ projectRoot: session.projectRoot || extras.projectRoot || null,
16
+ baseProjectRoot: session.baseProjectRoot || extras.baseProjectRoot || null,
17
+ worktreeIsolation:
18
+ session.worktreeIsolation === true || extras.worktreeIsolation === true,
19
+ worktree:
20
+ session.worktree || extras.worktree
21
+ ? createWorktreeRecord(session.worktree || extras.worktree, {
22
+ requested:
23
+ session.worktreeIsolation === true ||
24
+ extras.worktreeIsolation === true,
25
+ })
26
+ : null,
27
+ messageCount: extras.messageCount ?? history.length,
28
+ history,
29
+ status: extras.status || null,
30
+ };
31
+ }
@@ -0,0 +1,18 @@
1
+ export function createTaskRecord(task = {}, meta = {}) {
2
+ return {
3
+ id: task.id || null,
4
+ status: task.status || null,
5
+ type: task.type || null,
6
+ description: task.description || null,
7
+ ownerNodeId: task.ownerNodeId || null,
8
+ createdAt: task.createdAt || null,
9
+ startedAt: task.startedAt || null,
10
+ completedAt: task.completedAt || null,
11
+ result: task.result ?? null,
12
+ error: task.error ?? null,
13
+ recoveredFromRestart: Boolean(task.recoveredFromRestart),
14
+ recoverySourceStatus: task.recoverySourceStatus || null,
15
+ outputSummary: task.outputSummary || null,
16
+ meta,
17
+ };
18
+ }
@@ -0,0 +1,23 @@
1
+ export function createTelemetryRecord(metric = {}, meta = {}) {
2
+ return {
3
+ kind: metric.kind || "telemetry",
4
+ provider: metric.provider || null,
5
+ model: metric.model || null,
6
+ source: metric.source || null,
7
+ strategy: metric.strategy || null,
8
+ variant: metric.variant || metric.abVariant || null,
9
+ savedTokens:
10
+ typeof metric.savedTokens === "number"
11
+ ? metric.savedTokens
12
+ : typeof metric.saved === "number"
13
+ ? metric.saved
14
+ : null,
15
+ originalTokens:
16
+ typeof metric.originalTokens === "number" ? metric.originalTokens : null,
17
+ compressedTokens:
18
+ typeof metric.compressedTokens === "number" ? metric.compressedTokens : null,
19
+ ratio: typeof metric.ratio === "number" ? metric.ratio : null,
20
+ timestamp: metric.timestamp || Date.now(),
21
+ meta,
22
+ };
23
+ }
@@ -0,0 +1,14 @@
1
+ export function createWorktreeRecord(worktree = {}, meta = {}) {
2
+ return {
3
+ branch: worktree.branch || null,
4
+ path: worktree.path || worktree.worktreePath || null,
5
+ baseBranch: worktree.baseBranch || null,
6
+ hasChanges: typeof worktree.hasChanges === "boolean" ? worktree.hasChanges : null,
7
+ summary: worktree.summary || null,
8
+ conflicts: Array.isArray(worktree.conflicts) ? worktree.conflicts : [],
9
+ previewEntrypoints: Array.isArray(worktree.previewEntrypoints)
10
+ ? worktree.previewEntrypoints
11
+ : [],
12
+ meta,
13
+ };
14
+ }