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
@@ -0,0 +1,452 @@
1
+ /**
2
+ * JSONL Session Store — append-only session persistence.
3
+ */
4
+
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ appendFileSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ copyFileSync,
12
+ rmSync,
13
+ } from "node:fs";
14
+ import { join, basename, resolve } from "node:path";
15
+ import { createHash } from "node:crypto";
16
+ import { getHomeDir } from "../lib/paths.js";
17
+
18
+ function getSessionsDir() {
19
+ const dir = join(getHomeDir(), "sessions");
20
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
21
+ return dir;
22
+ }
23
+
24
+ function sessionPath(sessionId) {
25
+ return join(getSessionsDir(), `${sessionId}.jsonl`);
26
+ }
27
+
28
+ export function appendEvent(sessionId, type, data) {
29
+ const line = JSON.stringify({ type, timestamp: Date.now(), data }) + "\n";
30
+ appendFileSync(sessionPath(sessionId), line, "utf-8");
31
+ }
32
+
33
+ export function startSession(sessionId, meta = {}) {
34
+ const id =
35
+ sessionId ||
36
+ `session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
37
+
38
+ appendEvent(id, "session_start", {
39
+ title: meta.title || "Untitled",
40
+ provider: meta.provider || "",
41
+ model: meta.model || "",
42
+ });
43
+
44
+ return id;
45
+ }
46
+
47
+ export function appendUserMessage(sessionId, content) {
48
+ appendEvent(sessionId, "user_message", { role: "user", content });
49
+ }
50
+
51
+ export function appendAssistantMessage(sessionId, content) {
52
+ appendEvent(sessionId, "assistant_message", { role: "assistant", content });
53
+ }
54
+
55
+ export function appendToolCall(sessionId, toolName, args) {
56
+ appendEvent(sessionId, "tool_call", { tool: toolName, args });
57
+ }
58
+
59
+ export function appendToolResult(sessionId, toolName, result) {
60
+ appendEvent(sessionId, "tool_result", { tool: toolName, result });
61
+ }
62
+
63
+ export function appendCompactEvent(sessionId, stats) {
64
+ appendEvent(sessionId, "compact", stats);
65
+ }
66
+
67
+ export function readEvents(sessionId) {
68
+ const filePath = sessionPath(sessionId);
69
+ if (!existsSync(filePath)) return [];
70
+
71
+ const content = readFileSync(filePath, "utf-8");
72
+ const events = [];
73
+
74
+ for (const line of content.split("\n")) {
75
+ if (!line.trim()) continue;
76
+ try {
77
+ events.push(JSON.parse(line));
78
+ } catch (_e) {
79
+ // Skip malformed lines
80
+ }
81
+ }
82
+
83
+ return events;
84
+ }
85
+
86
+ export function rebuildMessages(sessionId) {
87
+ const events = readEvents(sessionId);
88
+ const messages = [];
89
+ let lastCompactIndex = -1;
90
+
91
+ for (let i = events.length - 1; i >= 0; i--) {
92
+ if (events[i].type === "compact" && events[i].data.messages) {
93
+ lastCompactIndex = i;
94
+ break;
95
+ }
96
+ }
97
+
98
+ if (lastCompactIndex >= 0 && events[lastCompactIndex].data.messages) {
99
+ messages.push(...events[lastCompactIndex].data.messages);
100
+ }
101
+
102
+ const startIndex = lastCompactIndex >= 0 ? lastCompactIndex + 1 : 0;
103
+
104
+ for (let i = startIndex; i < events.length; i++) {
105
+ const event = events[i];
106
+ if (
107
+ event.type === "user_message" ||
108
+ event.type === "assistant_message" ||
109
+ event.type === "system"
110
+ ) {
111
+ messages.push(event.data);
112
+ }
113
+ }
114
+
115
+ return messages;
116
+ }
117
+
118
+ export function listJsonlSessions(options = {}) {
119
+ const dir = getSessionsDir();
120
+ if (!existsSync(dir)) return [];
121
+
122
+ const limit = options.limit || 20;
123
+ const files = readdirSync(dir)
124
+ .filter((f) => f.endsWith(".jsonl"))
125
+ .map((f) => {
126
+ const id = basename(f, ".jsonl");
127
+ const events = readEvents(id);
128
+ const startEvent = events.find((e) => e.type === "session_start");
129
+ const lastEvent = events[events.length - 1];
130
+ const messageCount = events.filter(
131
+ (e) => e.type === "user_message" || e.type === "assistant_message",
132
+ ).length;
133
+
134
+ return {
135
+ id,
136
+ title: startEvent?.data?.title || "Untitled",
137
+ provider: startEvent?.data?.provider || "",
138
+ model: startEvent?.data?.model || "",
139
+ message_count: messageCount,
140
+ created_at: startEvent
141
+ ? new Date(startEvent.timestamp).toISOString()
142
+ : "",
143
+ updated_at: lastEvent
144
+ ? new Date(lastEvent.timestamp).toISOString()
145
+ : "",
146
+ };
147
+ })
148
+ .sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1))
149
+ .slice(0, limit);
150
+
151
+ return files;
152
+ }
153
+
154
+ export function forkSession(sourceId) {
155
+ const events = readEvents(sourceId);
156
+ if (events.length === 0) return null;
157
+
158
+ const newId = `session-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
159
+ const filePath = sessionPath(newId);
160
+
161
+ for (const event of events) {
162
+ const line = JSON.stringify(event) + "\n";
163
+ appendFileSync(filePath, line, "utf-8");
164
+ }
165
+
166
+ appendEvent(newId, "system", {
167
+ role: "system",
168
+ content: `[Forked from session ${sourceId}]`,
169
+ });
170
+
171
+ return newId;
172
+ }
173
+
174
+ export function sessionExists(sessionId) {
175
+ return existsSync(sessionPath(sessionId));
176
+ }
177
+
178
+ export function getLastSessionId() {
179
+ const sessions = listJsonlSessions({ limit: 1 });
180
+ return sessions.length > 0 ? sessions[0].id : null;
181
+ }
182
+
183
+ export function migrateLegacySessions(sourceDir, options = {}) {
184
+ return migrateLegacySessionsBatch(sourceDir, options).results;
185
+ }
186
+
187
+ export function migrateLegacySessionsBatch(sourceDir, options = {}) {
188
+ const directory = resolve(sourceDir || getSessionsDir());
189
+ if (!existsSync(directory)) {
190
+ throw new Error(`Directory not found: ${directory}`);
191
+ }
192
+
193
+ const files = readdirSync(directory).filter(
194
+ (file) =>
195
+ file.endsWith(".json") &&
196
+ !file.endsWith(".jsonl") &&
197
+ !file.endsWith(".migrated.json"),
198
+ );
199
+
200
+ const results = [];
201
+ for (const file of files) {
202
+ const filePath = join(directory, file);
203
+ results.push(migrateLegacySessionFile(filePath, options));
204
+ }
205
+
206
+ const summary = buildMigrationSummary(results, {
207
+ directory,
208
+ dryRun: Boolean(options.dryRun),
209
+ });
210
+ const sampledValidation = options.dryRun
211
+ ? []
212
+ : sampleMigratedSessionsValidation(results, {
213
+ sampleSize: options.sampleSize,
214
+ });
215
+
216
+ return {
217
+ directory,
218
+ results,
219
+ summary,
220
+ sampledValidation,
221
+ };
222
+ }
223
+
224
+ export function migrateLegacySessionFile(filePath, options = {}) {
225
+ const sourcePath = resolve(filePath);
226
+ const maxAttempts = Math.max(
227
+ 1,
228
+ (options.retryFailures ? 2 : 1) + Math.max(0, options.retryCount || 0),
229
+ );
230
+ let lastError = null;
231
+
232
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
233
+ try {
234
+ const result = performLegacySessionMigration(sourcePath, options);
235
+ return {
236
+ ...result,
237
+ attempts: attempt,
238
+ retried: attempt > 1,
239
+ };
240
+ } catch (error) {
241
+ lastError = error;
242
+ }
243
+ }
244
+
245
+ return {
246
+ file: sourcePath,
247
+ sessionId: basename(sourcePath, ".json"),
248
+ migrated: false,
249
+ failed: true,
250
+ dryRun: Boolean(options.dryRun),
251
+ attempts: maxAttempts,
252
+ reason: lastError?.message || "migration failed",
253
+ };
254
+ }
255
+
256
+ export function validateJsonlSession(sessionId) {
257
+ const filePath = sessionPath(sessionId);
258
+ if (!existsSync(filePath)) {
259
+ return {
260
+ sessionId,
261
+ valid: false,
262
+ reason: "session file not found",
263
+ malformedLines: 0,
264
+ eventCount: 0,
265
+ };
266
+ }
267
+
268
+ const lines = readFileSync(filePath, "utf-8").split("\n");
269
+ let malformedLines = 0;
270
+ const events = [];
271
+
272
+ for (const line of lines) {
273
+ if (!line.trim()) continue;
274
+ try {
275
+ events.push(JSON.parse(line));
276
+ } catch {
277
+ malformedLines++;
278
+ }
279
+ }
280
+
281
+ const hasStartEvent = events.some((event) => event.type === "session_start");
282
+ const messageCount = events.filter(
283
+ (event) =>
284
+ event.type === "user_message" || event.type === "assistant_message",
285
+ ).length;
286
+
287
+ return {
288
+ sessionId,
289
+ valid: malformedLines === 0 && hasStartEvent,
290
+ malformedLines,
291
+ eventCount: events.length,
292
+ messageCount,
293
+ hasStartEvent,
294
+ };
295
+ }
296
+
297
+ export function validateAllJsonlSessions(options = {}) {
298
+ const files = readdirSync(getSessionsDir())
299
+ .filter((file) => file.endsWith(".jsonl"))
300
+ .slice(0, options.limit || 1000);
301
+ return files.map((file) => validateJsonlSession(basename(file, ".jsonl")));
302
+ }
303
+
304
+ export function sampleMigratedSessionsValidation(results, options = {}) {
305
+ const sampleSize = Math.max(0, parseInt(options.sampleSize || 3, 10));
306
+ const migrated = results.filter((item) => item.migrated && !item.dryRun);
307
+ return migrated.slice(0, sampleSize).map((item) => {
308
+ const validation = validateJsonlSession(item.sessionId);
309
+ return {
310
+ sessionId: item.sessionId,
311
+ file: item.file,
312
+ valid: validation.valid,
313
+ messageCount: validation.messageCount,
314
+ expectedMessageCount: item.messageCount,
315
+ matchesExpectedMessages: validation.messageCount === item.messageCount,
316
+ malformedLines: validation.malformedLines,
317
+ };
318
+ });
319
+ }
320
+
321
+ function performLegacySessionMigration(sourcePath, options) {
322
+ const parsed = JSON.parse(readFileSync(sourcePath, "utf-8"));
323
+ const legacy = normalizeLegacySession(parsed, basename(sourcePath, ".json"));
324
+ const sessionId = legacy.id;
325
+
326
+ if (!options.force && sessionExists(sessionId)) {
327
+ return {
328
+ file: sourcePath,
329
+ sessionId,
330
+ skipped: true,
331
+ reason: "jsonl session already exists",
332
+ };
333
+ }
334
+
335
+ if (!options.dryRun) {
336
+ if (options.force && sessionExists(sessionId)) {
337
+ rmSync(sessionPath(sessionId), { force: true });
338
+ }
339
+ startSession(sessionId, legacy.meta);
340
+ for (const message of legacy.messages) {
341
+ appendLegacyMessage(sessionId, message);
342
+ }
343
+ if (legacy.summary) {
344
+ appendEvent(sessionId, "system", {
345
+ role: "system",
346
+ content: `[Migrated Summary]\n${legacy.summary}`,
347
+ });
348
+ }
349
+
350
+ const validation = validateJsonlSession(sessionId);
351
+ if (!validation.valid || validation.messageCount !== legacy.messages.length) {
352
+ throw new Error(
353
+ `post-migration validation failed for ${sessionId} (${validation.messageCount}/${legacy.messages.length} messages)`,
354
+ );
355
+ }
356
+
357
+ if (options.archive !== false) {
358
+ copyFileSync(sourcePath, `${sourcePath}.migrated.json`);
359
+ }
360
+ }
361
+
362
+ return {
363
+ file: sourcePath,
364
+ sessionId,
365
+ migrated: true,
366
+ messageCount: legacy.messages.length,
367
+ archived: options.archive !== false && !options.dryRun,
368
+ dryRun: Boolean(options.dryRun),
369
+ };
370
+ }
371
+
372
+ function buildMigrationSummary(results, options = {}) {
373
+ const summary = {
374
+ directory: options.directory || null,
375
+ dryRun: Boolean(options.dryRun),
376
+ scanned: results.length,
377
+ migrated: 0,
378
+ skipped: 0,
379
+ failed: 0,
380
+ retries: 0,
381
+ };
382
+
383
+ for (const result of results) {
384
+ if (result.migrated) summary.migrated += 1;
385
+ if (result.skipped) summary.skipped += 1;
386
+ if (result.failed) summary.failed += 1;
387
+ if (result.retried) summary.retries += 1;
388
+ }
389
+
390
+ return summary;
391
+ }
392
+
393
+ function normalizeLegacySession(payload, fallbackId) {
394
+ const messages = Array.isArray(payload)
395
+ ? payload
396
+ : Array.isArray(payload?.messages)
397
+ ? payload.messages
398
+ : [];
399
+
400
+ return {
401
+ id: payload?.id || fallbackId || `session-${Date.now()}`,
402
+ meta: {
403
+ title: payload?.title || payload?.name || fallbackId || "Migrated Session",
404
+ provider: payload?.provider || "",
405
+ model: payload?.model || "",
406
+ },
407
+ summary: payload?.summary || "",
408
+ messages: messages.map(normalizeLegacyMessage).filter(Boolean),
409
+ };
410
+ }
411
+
412
+ function normalizeLegacyMessage(message) {
413
+ if (!message) return null;
414
+ if (typeof message === "string") {
415
+ return { role: "user", content: message };
416
+ }
417
+
418
+ const role = message.role || message.sender || message.type || "user";
419
+ const content =
420
+ message.content ??
421
+ message.text ??
422
+ message.message ??
423
+ message.result ??
424
+ "";
425
+
426
+ return {
427
+ role,
428
+ content: typeof content === "string" ? content : JSON.stringify(content),
429
+ tool: message.tool || message.name || null,
430
+ args: message.args || message.arguments || null,
431
+ };
432
+ }
433
+
434
+ function appendLegacyMessage(sessionId, message) {
435
+ switch (message.role) {
436
+ case "assistant":
437
+ appendAssistantMessage(sessionId, message.content);
438
+ break;
439
+ case "tool":
440
+ appendToolResult(sessionId, message.tool || "legacy-tool", message.content);
441
+ break;
442
+ case "system":
443
+ appendEvent(sessionId, "system", {
444
+ role: "system",
445
+ content: message.content,
446
+ });
447
+ break;
448
+ default:
449
+ appendUserMessage(sessionId, message.content);
450
+ break;
451
+ }
452
+ }