chainlesschain 0.45.11 → 0.45.19

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 (81) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/AppLayout-B00RARl2.js +1 -0
  3. package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
  4. package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DXtvKoM0.js} +1 -1
  5. package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
  6. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  7. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  8. package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
  9. package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
  10. package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
  11. package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
  12. package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
  13. package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
  14. package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
  15. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  16. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  17. package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
  18. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  19. package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
  20. package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
  21. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  22. package/src/assets/web-panel/index.html +2 -2
  23. package/src/commands/agent.js +7 -8
  24. package/src/commands/chat.js +9 -11
  25. package/src/commands/serve.js +11 -106
  26. package/src/commands/session.js +185 -18
  27. package/src/commands/ui.js +10 -151
  28. package/src/gateways/repl/agent-repl.js +1 -0
  29. package/src/gateways/repl/chat-repl.js +1 -0
  30. package/src/gateways/ui/web-ui-server.js +1 -0
  31. package/src/gateways/ws/action-protocol.js +83 -0
  32. package/src/gateways/ws/message-dispatcher.js +73 -0
  33. package/src/gateways/ws/session-protocol.js +396 -0
  34. package/src/gateways/ws/task-protocol.js +55 -0
  35. package/src/gateways/ws/worktree-protocol.js +315 -0
  36. package/src/gateways/ws/ws-server.js +4 -0
  37. package/src/gateways/ws/ws-session-gateway.js +1 -0
  38. package/src/harness/background-task-manager.js +506 -0
  39. package/src/harness/background-task-worker.js +48 -0
  40. package/src/harness/compression-telemetry.js +214 -0
  41. package/src/harness/feature-flags.js +157 -0
  42. package/src/harness/jsonl-session-store.js +452 -0
  43. package/src/harness/prompt-compressor.js +416 -0
  44. package/src/harness/worktree-isolator.js +845 -0
  45. package/src/lib/agent-core.js +246 -45
  46. package/src/lib/background-task-manager.js +1 -305
  47. package/src/lib/background-task-worker.js +1 -50
  48. package/src/lib/compression-telemetry.js +5 -0
  49. package/src/lib/feature-flags.js +7 -182
  50. package/src/lib/interaction-adapter.js +32 -6
  51. package/src/lib/jsonl-session-store.js +21 -237
  52. package/src/lib/prompt-compressor.js +10 -351
  53. package/src/lib/sub-agent-context.js +91 -0
  54. package/src/lib/worktree-isolator.js +13 -231
  55. package/src/lib/ws-agent-handler.js +1 -0
  56. package/src/lib/ws-server.js +155 -359
  57. package/src/lib/ws-session-manager.js +82 -1
  58. package/src/repl/agent-repl.js +114 -32
  59. package/src/runtime/agent-runtime.js +417 -0
  60. package/src/runtime/contracts/agent-turn.js +11 -0
  61. package/src/runtime/contracts/session-record.js +31 -0
  62. package/src/runtime/contracts/task-record.js +18 -0
  63. package/src/runtime/contracts/telemetry-record.js +23 -0
  64. package/src/runtime/contracts/worktree-record.js +14 -0
  65. package/src/runtime/index.js +13 -0
  66. package/src/runtime/policies/agent-policy.js +45 -0
  67. package/src/runtime/runtime-context.js +14 -0
  68. package/src/runtime/runtime-events.js +37 -0
  69. package/src/runtime/runtime-factory.js +50 -0
  70. package/src/tools/index.js +22 -0
  71. package/src/tools/legacy-agent-tools.js +171 -0
  72. package/src/tools/registry.js +141 -0
  73. package/src/tools/tool-context.js +28 -0
  74. package/src/tools/tool-permissions.js +28 -0
  75. package/src/tools/tool-telemetry.js +39 -0
  76. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
  77. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
  78. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  79. package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
  80. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  81. 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
  }
@@ -29,6 +29,14 @@ import {
29
29
  saveMessages,
30
30
  getSession,
31
31
  } from "../lib/session-manager.js";
32
+ import {
33
+ startSession as jsonlStartSession,
34
+ appendUserMessage,
35
+ appendAssistantMessage,
36
+ appendCompactEvent,
37
+ rebuildMessages,
38
+ sessionExists,
39
+ } from "../lib/jsonl-session-store.js";
32
40
  import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
33
41
  import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
34
42
  import { createChatFn } from "../lib/cowork-adapter.js";
@@ -40,6 +48,7 @@ import { CLIPermanentMemory } from "../lib/permanent-memory.js";
40
48
  import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
41
49
  import { PromptCompressor } from "../lib/prompt-compressor.js";
42
50
  import { feature } from "../lib/feature-flags.js";
51
+ import { recordCompressionMetric } from "../lib/compression-telemetry.js";
43
52
  import {
44
53
  AGENT_TOOLS,
45
54
  buildSystemPrompt,
@@ -108,9 +117,9 @@ export async function startAgentRepl(options = {}) {
108
117
  // Continue without DB — static prompt fallback
109
118
  }
110
119
 
111
- // Initialize prompt compressor
120
+ // Initialize prompt compressor (adaptive to model's context window)
112
121
  if (feature("PROMPT_COMPRESSOR")) {
113
- _compressor = new PromptCompressor({ maxMessages: 20, maxTokens: 8000 });
122
+ _compressor = new PromptCompressor({ model, provider });
114
123
  }
115
124
 
116
125
  // Initialize permanent memory
@@ -133,7 +142,18 @@ export async function startAgentRepl(options = {}) {
133
142
  _hookDb = db;
134
143
 
135
144
  // Resume existing session or create new one
136
- if (db && options.sessionId) {
145
+ const useJsonl = feature("JSONL_SESSION");
146
+
147
+ if (useJsonl && options.sessionId) {
148
+ // JSONL resume: check if session file exists
149
+ try {
150
+ if (sessionExists(options.sessionId)) {
151
+ sessionId = options.sessionId;
152
+ }
153
+ } catch (_err) {
154
+ // Non-critical
155
+ }
156
+ } else if (db && options.sessionId) {
137
157
  try {
138
158
  const existing = getSession(db, options.sessionId);
139
159
  if (existing && existing.messages) {
@@ -144,16 +164,25 @@ export async function startAgentRepl(options = {}) {
144
164
  }
145
165
  }
146
166
 
147
- if (db && !sessionId) {
148
- try {
149
- const session = createSession(db, {
150
- title: `Agent ${new Date().toISOString().slice(0, 10)}`,
151
- provider,
152
- model,
153
- });
154
- sessionId = session.id;
155
- } catch (_err) {
156
- // Non-critical
167
+ if (!sessionId) {
168
+ const meta = {
169
+ title: `Agent ${new Date().toISOString().slice(0, 10)}`,
170
+ provider,
171
+ model,
172
+ };
173
+ if (useJsonl) {
174
+ try {
175
+ sessionId = jsonlStartSession(null, meta);
176
+ } catch (_err) {
177
+ // Non-critical
178
+ }
179
+ } else if (db) {
180
+ try {
181
+ const session = createSession(db, meta);
182
+ sessionId = session.id;
183
+ } catch (_err) {
184
+ // Non-critical
185
+ }
157
186
  }
158
187
  }
159
188
 
@@ -162,16 +191,28 @@ export async function startAgentRepl(options = {}) {
162
191
  ];
163
192
 
164
193
  // Load resumed session messages
165
- if (db && options.sessionId && sessionId) {
194
+ if (options.sessionId && sessionId) {
166
195
  try {
167
- const existing = getSession(db, sessionId);
168
- if (existing && existing.messages) {
169
- const parsed =
170
- typeof existing.messages === "string"
171
- ? JSON.parse(existing.messages)
172
- : existing.messages;
173
- messages.push(...parsed.filter((m) => m.role !== "system"));
174
- logger.info(`Resumed session ${sessionId} (${parsed.length} messages)`);
196
+ if (useJsonl) {
197
+ const rebuilt = rebuildMessages(sessionId);
198
+ if (rebuilt.length > 0) {
199
+ messages.push(...rebuilt.filter((m) => m.role !== "system"));
200
+ logger.info(
201
+ `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
202
+ );
203
+ }
204
+ } else if (db) {
205
+ const existing = getSession(db, sessionId);
206
+ if (existing && existing.messages) {
207
+ const parsed =
208
+ typeof existing.messages === "string"
209
+ ? JSON.parse(existing.messages)
210
+ : existing.messages;
211
+ messages.push(...parsed.filter((m) => m.role !== "system"));
212
+ logger.info(
213
+ `Resumed session ${sessionId} (${parsed.length} messages)`,
214
+ );
215
+ }
175
216
  }
176
217
  } catch (_err) {
177
218
  // Non-critical
@@ -386,6 +427,11 @@ export async function startAgentRepl(options = {}) {
386
427
  await _compressor.compress(messages);
387
428
  messages.length = 0;
388
429
  messages.push(...compacted);
430
+ recordCompressionMetric(stats, {
431
+ source: "manual-compact",
432
+ provider,
433
+ model,
434
+ });
389
435
  logger.info(
390
436
  `Compacted: ${stats.originalMessages} → ${stats.compressedMessages} messages, saved ${stats.saved} tokens (${stats.strategy})`,
391
437
  );
@@ -434,10 +480,16 @@ export async function startAgentRepl(options = {}) {
434
480
  const sessionArg = trimmed.slice(8).trim();
435
481
  if (sessionArg.startsWith("resume ")) {
436
482
  const resumeId = sessionArg.slice(7).trim();
437
- if (!db) {
438
- logger.info("No database available for session resume");
439
- } else {
440
- try {
483
+ try {
484
+ if (useJsonl && sessionExists(resumeId)) {
485
+ const rebuilt = rebuildMessages(resumeId);
486
+ messages.length = 1; // keep system prompt
487
+ messages.push(...rebuilt.filter((m) => m.role !== "system"));
488
+ sessionId = resumeId;
489
+ logger.info(
490
+ `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
491
+ );
492
+ } else if (db) {
441
493
  const existing = getSession(db, resumeId);
442
494
  if (existing && existing.messages) {
443
495
  const parsed =
@@ -453,9 +505,11 @@ export async function startAgentRepl(options = {}) {
453
505
  } else {
454
506
  logger.info(`Session not found: ${resumeId}`);
455
507
  }
456
- } catch (err) {
457
- logger.error(`Resume failed: ${err.message}`);
508
+ } else {
509
+ logger.info("No session store available");
458
510
  }
511
+ } catch (err) {
512
+ logger.error(`Resume failed: ${err.message}`);
459
513
  }
460
514
  } else {
461
515
  logger.info(`Session ID: ${sessionId || "none"}`);
@@ -1052,9 +1106,17 @@ export async function startAgentRepl(options = {}) {
1052
1106
  }
1053
1107
 
1054
1108
  // Auto-save session
1055
- if (db && sessionId) {
1109
+ if (sessionId) {
1056
1110
  try {
1057
- saveMessages(db, sessionId, messages);
1111
+ if (useJsonl) {
1112
+ // Append incremental events (user + assistant)
1113
+ appendUserMessage(sessionId, trimmed);
1114
+ if (response) {
1115
+ appendAssistantMessage(sessionId, response);
1116
+ }
1117
+ } else if (db) {
1118
+ saveMessages(db, sessionId, messages);
1119
+ }
1058
1120
  } catch (_e) {
1059
1121
  // Non-critical
1060
1122
  }
@@ -1070,10 +1132,22 @@ export async function startAgentRepl(options = {}) {
1070
1132
  await _compressor.compress(messages);
1071
1133
  messages.length = 0;
1072
1134
  messages.push(...compacted);
1135
+ recordCompressionMetric(stats, {
1136
+ source: "auto-compact",
1137
+ provider,
1138
+ model: activeModel,
1139
+ });
1073
1140
  if (stats.saved > 0) {
1074
1141
  logger.verbose(
1075
1142
  `Auto-compacted: ${stats.strategy} (saved ${stats.saved} tokens)`,
1076
1143
  );
1144
+ // Write compact checkpoint to JSONL for crash recovery
1145
+ if (useJsonl && sessionId) {
1146
+ appendCompactEvent(sessionId, {
1147
+ ...stats,
1148
+ messages: compacted,
1149
+ });
1150
+ }
1077
1151
  }
1078
1152
  } catch (_e) {
1079
1153
  // Non-critical — continue with uncompacted messages
@@ -1116,9 +1190,17 @@ export async function startAgentRepl(options = {}) {
1116
1190
 
1117
1191
  rl.on("close", async () => {
1118
1192
  // Save session on exit
1119
- if (db && sessionId) {
1193
+ if (sessionId) {
1120
1194
  try {
1121
- saveMessages(db, sessionId, messages);
1195
+ if (useJsonl) {
1196
+ // JSONL: write final compact snapshot for fast rebuild
1197
+ appendCompactEvent(sessionId, {
1198
+ strategy: "session-end",
1199
+ messages,
1200
+ });
1201
+ } else if (db) {
1202
+ saveMessages(db, sessionId, messages);
1203
+ }
1122
1204
  } catch (_e) {
1123
1205
  // Non-critical
1124
1206
  }