chainlesschain 0.45.11 → 0.45.12

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/AppLayout-BfLjLMsm.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-DP7PO9Li.js} +1 -1
  5. package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-DyQF-7R1.js} +1 -1
  6. package/src/assets/web-panel/assets/{Dashboard-DsjXpZor.js → Dashboard-BGGdnr6t.js} +2 -2
  7. package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-BOii-AoO.js} +1 -1
  8. package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-DmiJtJYr.js} +2 -2
  9. package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-CDRMMobU.js} +2 -2
  10. package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-CVhqqoS1.js} +1 -1
  11. package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dkt7021l.js} +1 -1
  12. package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-DUDL_UGb.js} +1 -1
  13. package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-DXXELJc3.js} +1 -1
  14. package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +1 -0
  15. package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +1 -0
  16. package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
  17. package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-vW799KpE.js} +2 -2
  18. package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
  19. package/src/assets/web-panel/index.html +2 -2
  20. package/src/commands/session.js +84 -18
  21. package/src/lib/prompt-compressor.js +132 -2
  22. package/src/lib/sub-agent-context.js +71 -0
  23. package/src/lib/ws-server.js +45 -0
  24. package/src/repl/agent-repl.js +103 -32
  25. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
  26. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
@@ -8,9 +8,9 @@
8
8
  // Injected by web-ui-server.js at serve time
9
9
  window.__CC_CONFIG__ = __CC_CONFIG_PLACEHOLDER__;
10
10
  </script>
11
- <script type="module" crossorigin src="./assets/index-DQ5xXK7O.js"></script>
11
+ <script type="module" crossorigin src="./assets/index-vW799KpE.js"></script>
12
12
  <link rel="modulepreload" crossorigin href="./assets/vendor-CN0Iv_qZ.js">
13
- <link rel="modulepreload" crossorigin href="./assets/antd-ChLPLhSn.js">
13
+ <link rel="modulepreload" crossorigin href="./assets/antd-CJSBocer.js">
14
14
  <link rel="stylesheet" crossorigin href="./assets/index-CyGyEIVX.css">
15
15
  </head>
16
16
  <body>
@@ -13,6 +13,13 @@ import {
13
13
  deleteSession,
14
14
  exportSessionMarkdown,
15
15
  } from "../lib/session-manager.js";
16
+ import {
17
+ listJsonlSessions,
18
+ rebuildMessages,
19
+ sessionExists,
20
+ readEvents,
21
+ } from "../lib/jsonl-session-store.js";
22
+ import { feature } from "../lib/feature-flags.js";
16
23
 
17
24
  export function registerSessionCommand(program) {
18
25
  const session = program
@@ -28,14 +35,39 @@ export function registerSessionCommand(program) {
28
35
  .action(async (options) => {
29
36
  try {
30
37
  const ctx = await bootstrap({ verbose: program.opts().verbose });
31
- if (!ctx.db) {
32
- logger.error("Database not available");
33
- process.exit(1);
38
+ const limit = Math.max(1, parseInt(options.limit) || 20);
39
+ let sessions = [];
40
+
41
+ // Merge DB sessions + JSONL sessions
42
+ if (ctx.db) {
43
+ const db = ctx.db.getDatabase();
44
+ sessions.push(
45
+ ...listSessions(db, { limit }).map((s) => ({
46
+ ...s,
47
+ _store: "db",
48
+ })),
49
+ );
50
+ }
51
+
52
+ if (feature("JSONL_SESSION")) {
53
+ sessions.push(
54
+ ...listJsonlSessions({ limit }).map((s) => ({
55
+ ...s,
56
+ _store: "jsonl",
57
+ })),
58
+ );
34
59
  }
35
- const db = ctx.db.getDatabase();
36
- const sessions = listSessions(db, {
37
- limit: Math.max(1, parseInt(options.limit) || 20),
38
- });
60
+
61
+ // Deduplicate by id (JSONL takes precedence), sort by updated_at
62
+ const seen = new Set();
63
+ sessions = sessions
64
+ .sort((a, b) => (b.updated_at > a.updated_at ? 1 : -1))
65
+ .filter((s) => {
66
+ if (seen.has(s.id)) return false;
67
+ seen.add(s.id);
68
+ return true;
69
+ })
70
+ .slice(0, limit);
39
71
 
40
72
  if (options.json) {
41
73
  console.log(JSON.stringify(sessions, null, 2));
@@ -46,8 +78,10 @@ export function registerSessionCommand(program) {
46
78
  } else {
47
79
  logger.log(chalk.bold(`Sessions (${sessions.length}):\n`));
48
80
  for (const s of sessions) {
81
+ const storeTag =
82
+ s._store === "jsonl" ? chalk.yellow("[JSONL]") : "";
49
83
  logger.log(
50
- ` ${chalk.gray(s.id.slice(0, 16))} ${chalk.white(s.title)} ${chalk.cyan(s.message_count + " msgs")} ${chalk.gray(s.updated_at)}`,
84
+ ` ${chalk.gray(s.id.slice(0, 16))} ${chalk.white(s.title)} ${chalk.cyan(s.message_count + " msgs")} ${chalk.gray(s.updated_at)} ${storeTag}`,
51
85
  );
52
86
  if (s.summary) {
53
87
  logger.log(` ${chalk.gray(s.summary.substring(0, 100))}`);
@@ -72,12 +106,29 @@ export function registerSessionCommand(program) {
72
106
  .action(async (id, options) => {
73
107
  try {
74
108
  const ctx = await bootstrap({ verbose: program.opts().verbose });
75
- if (!ctx.db) {
76
- logger.error("Database not available");
77
- process.exit(1);
109
+ let sess = null;
110
+
111
+ // Try JSONL first if enabled
112
+ if (feature("JSONL_SESSION") && sessionExists(id)) {
113
+ const events = readEvents(id);
114
+ const startEvent = events.find((e) => e.type === "session_start");
115
+ const msgs = rebuildMessages(id);
116
+ sess = {
117
+ id,
118
+ title: startEvent?.data?.title || "Untitled",
119
+ provider: startEvent?.data?.provider || "",
120
+ model: startEvent?.data?.model || "",
121
+ message_count: msgs.length,
122
+ messages: msgs,
123
+ _store: "jsonl",
124
+ };
125
+ }
126
+
127
+ // Fallback to DB
128
+ if (!sess && ctx.db) {
129
+ const db = ctx.db.getDatabase();
130
+ sess = getSession(db, id);
78
131
  }
79
- const db = ctx.db.getDatabase();
80
- const sess = getSession(db, id);
81
132
 
82
133
  if (!sess) {
83
134
  logger.error(`Session not found: ${id}`);
@@ -127,12 +178,27 @@ export function registerSessionCommand(program) {
127
178
  .action(async (id, options) => {
128
179
  try {
129
180
  const ctx = await bootstrap({ verbose: program.opts().verbose });
130
- if (!ctx.db) {
131
- logger.error("Database not available");
132
- process.exit(1);
181
+ let sess = null;
182
+
183
+ // Try JSONL first
184
+ if (feature("JSONL_SESSION") && sessionExists(id)) {
185
+ const events = readEvents(id);
186
+ const startEvent = events.find((e) => e.type === "session_start");
187
+ sess = {
188
+ id,
189
+ title: startEvent?.data?.title || "Untitled",
190
+ provider: startEvent?.data?.provider || "",
191
+ model: startEvent?.data?.model || "",
192
+ messages: rebuildMessages(id),
193
+ };
194
+ sess.message_count = sess.messages.length;
195
+ }
196
+
197
+ // Fallback to DB
198
+ if (!sess && ctx.db) {
199
+ const db = ctx.db.getDatabase();
200
+ sess = getSession(db, id);
133
201
  }
134
- const db = ctx.db.getDatabase();
135
- const sess = getSession(db, id);
136
202
 
137
203
  if (!sess) {
138
204
  logger.error(`Session not found: ${id}`);
@@ -60,6 +60,105 @@ function getContent(msg) {
60
60
  : JSON.stringify(msg.content || "");
61
61
  }
62
62
 
63
+ // ── Provider context window registry ───────────────────────────────────
64
+
65
+ /**
66
+ * Known context window sizes (in tokens) per model/provider.
67
+ * Used by adaptive compression to auto-tune maxTokens.
68
+ */
69
+ export const CONTEXT_WINDOWS = {
70
+ // Ollama local models
71
+ "qwen2.5:7b": 32768,
72
+ "qwen2.5:14b": 32768,
73
+ "qwen2.5-coder:14b": 32768,
74
+ "qwen2:7b": 32768,
75
+ "llama3:8b": 8192,
76
+ "mistral:7b": 32768,
77
+ "codellama:7b": 16384,
78
+ // OpenAI
79
+ "gpt-4o": 128000,
80
+ "gpt-4o-mini": 128000,
81
+ "gpt-4-turbo": 128000,
82
+ "gpt-3.5-turbo": 16385,
83
+ o1: 200000,
84
+ // Anthropic
85
+ "claude-opus-4-6": 200000,
86
+ "claude-sonnet-4-6": 200000,
87
+ "claude-haiku-4-5-20251001": 200000,
88
+ // DeepSeek
89
+ "deepseek-chat": 64000,
90
+ "deepseek-coder": 64000,
91
+ "deepseek-reasoner": 64000,
92
+ // DashScope
93
+ "qwen-turbo": 131072,
94
+ "qwen-plus": 131072,
95
+ "qwen-max": 32768,
96
+ // Gemini
97
+ "gemini-2.0-flash": 1048576,
98
+ "gemini-2.0-pro": 1048576,
99
+ "gemini-1.5-flash": 1048576,
100
+ // Kimi
101
+ "moonshot-v1-auto": 131072,
102
+ "moonshot-v1-8k": 8192,
103
+ "moonshot-v1-32k": 32768,
104
+ "moonshot-v1-128k": 131072,
105
+ // Volcengine
106
+ "doubao-seed-1-6-251015": 32768,
107
+ // Provider-level defaults (fallback when model not listed)
108
+ _provider_defaults: {
109
+ ollama: 32768,
110
+ openai: 128000,
111
+ anthropic: 200000,
112
+ deepseek: 64000,
113
+ dashscope: 131072,
114
+ gemini: 1048576,
115
+ kimi: 131072,
116
+ volcengine: 32768,
117
+ minimax: 32768,
118
+ mistral: 32768,
119
+ },
120
+ };
121
+
122
+ /**
123
+ * Get context window size for a model/provider combination.
124
+ * @param {string} [model] — Model name
125
+ * @param {string} [provider] — Provider name
126
+ * @returns {number} Context window in tokens
127
+ */
128
+ export function getContextWindow(model, provider) {
129
+ if (model && CONTEXT_WINDOWS[model]) {
130
+ return CONTEXT_WINDOWS[model];
131
+ }
132
+ if (provider && CONTEXT_WINDOWS._provider_defaults[provider]) {
133
+ return CONTEXT_WINDOWS._provider_defaults[provider];
134
+ }
135
+ return 32768; // Conservative default
136
+ }
137
+
138
+ /**
139
+ * Calculate adaptive compression thresholds based on context window.
140
+ *
141
+ * Strategy:
142
+ * - maxTokens = 60% of context window (reserve 40% for system prompt + response)
143
+ * - maxMessages scales with context: small ctx → 15, large ctx → 50
144
+ * - For very large contexts (>128k), enable less aggressive compression
145
+ *
146
+ * @param {number} contextWindow — Context window in tokens
147
+ * @returns {{ maxMessages: number, maxTokens: number, aggressive: boolean }}
148
+ */
149
+ export function adaptiveThresholds(contextWindow) {
150
+ const maxTokens = Math.floor(contextWindow * 0.6);
151
+ // Scale messages: 15 for 8k, 20 for 32k, 30 for 128k, 50 for 200k+
152
+ const maxMessages = Math.min(
153
+ 50,
154
+ Math.max(15, Math.floor(10 + Math.log2(contextWindow / 1024) * 5)),
155
+ );
156
+ // Aggressive compression only for small context windows
157
+ const aggressive = contextWindow < 32768;
158
+
159
+ return { maxMessages, maxTokens, aggressive };
160
+ }
161
+
63
162
  // ── PromptCompressor class ──────────────────────────────────────────────
64
163
 
65
164
  export class PromptCompressor {
@@ -69,14 +168,45 @@ export class PromptCompressor {
69
168
  * @param {number} [options.maxTokens=8000] — Token threshold for auto-compact
70
169
  * @param {number} [options.similarityThreshold=0.9] — Dedup similarity threshold
71
170
  * @param {Function} [options.llmQuery] — async (prompt) => string, for summarization
171
+ * @param {string} [options.model] — Model name (for adaptive thresholds)
172
+ * @param {string} [options.provider] — Provider name (for adaptive thresholds)
72
173
  */
73
174
  constructor(options = {}) {
74
- this.maxMessages = options.maxMessages || 20;
75
- this.maxTokens = options.maxTokens || 8000;
175
+ // If model/provider supplied and no explicit maxMessages/maxTokens, auto-adapt
176
+ if (
177
+ (options.model || options.provider) &&
178
+ !options.maxMessages &&
179
+ !options.maxTokens
180
+ ) {
181
+ const ctxWindow = getContextWindow(options.model, options.provider);
182
+ const adaptive = adaptiveThresholds(ctxWindow);
183
+ this.maxMessages = adaptive.maxMessages;
184
+ this.maxTokens = adaptive.maxTokens;
185
+ this._adaptive = true;
186
+ this._contextWindow = ctxWindow;
187
+ } else {
188
+ this.maxMessages = options.maxMessages || 20;
189
+ this.maxTokens = options.maxTokens || 8000;
190
+ this._adaptive = false;
191
+ this._contextWindow = null;
192
+ }
76
193
  this.similarityThreshold = options.similarityThreshold || 0.9;
77
194
  this.llmQuery = options.llmQuery || null;
78
195
  }
79
196
 
197
+ /**
198
+ * Reconfigure thresholds for a new model/provider (e.g. after model switch).
199
+ * Only updates if no explicit overrides were set in constructor.
200
+ */
201
+ adaptToModel(model, provider) {
202
+ const ctxWindow = getContextWindow(model, provider);
203
+ const adaptive = adaptiveThresholds(ctxWindow);
204
+ this.maxMessages = adaptive.maxMessages;
205
+ this.maxTokens = adaptive.maxTokens;
206
+ this._adaptive = true;
207
+ this._contextWindow = ctxWindow;
208
+ }
209
+
80
210
  /**
81
211
  * Run all enabled compression strategies on messages.
82
212
  * Returns { messages, stats }.
@@ -11,6 +11,13 @@
11
11
  import crypto from "crypto";
12
12
  import { CLIContextEngineering } from "./cli-context-engineering.js";
13
13
  import { agentLoop, buildSystemPrompt, AGENT_TOOLS } from "./agent-core.js";
14
+ import { feature } from "./feature-flags.js";
15
+ import {
16
+ createWorktree,
17
+ removeWorktree,
18
+ isolateTask,
19
+ } from "./worktree-isolator.js";
20
+ import { isGitRepo } from "./git-integration.js";
14
21
 
15
22
  // ─── Constants ──────────────────────────────────────────────────────────────
16
23
 
@@ -38,6 +45,7 @@ export class SubAgentContext {
38
45
  * @param {object} [options.permanentMemory] - Permanent memory instance
39
46
  * @param {object} [options.llmOptions] - LLM provider/model/key options
40
47
  * @param {string} [options.cwd] - Working directory
48
+ * @param {boolean} [options.useWorktree] - Force worktree isolation (overrides flag)
41
49
  * @returns {SubAgentContext}
42
50
  */
43
51
  static create(options = {}) {
@@ -59,6 +67,12 @@ export class SubAgentContext {
59
67
  this.createdAt = new Date().toISOString();
60
68
  this.completedAt = null;
61
69
 
70
+ // Worktree isolation state
71
+ this._useWorktree = options.useWorktree ?? feature("WORKTREE_ISOLATION");
72
+ this._worktreePath = null;
73
+ this._worktreeBranch = null;
74
+ this._repoDir = this.cwd;
75
+
62
76
  // ── Isolated state ──────────────────────────────────────────────
63
77
  // Independent message history — never shared with parent
64
78
  this.messages = [];
@@ -110,6 +124,60 @@ export class SubAgentContext {
110
124
  );
111
125
  }
112
126
 
127
+ // If worktree isolation is enabled, wrap execution in isolated worktree
128
+ if (this._useWorktree && isGitRepo(this._repoDir)) {
129
+ return this._runInWorktree(userPrompt, loopOptions);
130
+ }
131
+
132
+ return this._runCore(userPrompt, loopOptions);
133
+ }
134
+
135
+ /**
136
+ * Run in an isolated git worktree. Creates worktree → runs → cleans up.
137
+ */
138
+ async _runInWorktree(userPrompt, loopOptions = {}) {
139
+ const taskId = `${this.role}-${this.id.slice(4)}`;
140
+ try {
141
+ const { result, branch, worktreePath, hasChanges } = await isolateTask(
142
+ this._repoDir,
143
+ taskId,
144
+ async (wtPath) => {
145
+ this._worktreePath = wtPath;
146
+ this._worktreeBranch = `agent/${taskId}`;
147
+ // Override cwd to worktree for tool execution
148
+ this.cwd = wtPath;
149
+ return this._runCore(userPrompt, loopOptions);
150
+ },
151
+ );
152
+
153
+ // Annotate result with worktree info
154
+ if (result) {
155
+ result.worktree = {
156
+ branch,
157
+ path: worktreePath,
158
+ hasChanges,
159
+ };
160
+ }
161
+ return result;
162
+ } catch (err) {
163
+ // If worktree creation fails (e.g. not a git repo), fall back to direct
164
+ this.status = "failed";
165
+ this.completedAt = new Date().toISOString();
166
+ this.result = {
167
+ summary: `Worktree isolation failed: ${err.message}`,
168
+ artifacts: [],
169
+ tokenCount: this._tokenCount,
170
+ toolsUsed: [...new Set(this._toolsUsed)],
171
+ iterationCount: this._iterationCount,
172
+ };
173
+ return this.result;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Core agent loop execution (shared by direct and worktree paths).
179
+ */
180
+ async _runCore(userPrompt, loopOptions = {}) {
113
181
  // Add user message
114
182
  this.messages.push({ role: "user", content: userPrompt });
115
183
 
@@ -291,6 +359,9 @@ export class SubAgentContext {
291
359
  iterationCount: this._iterationCount,
292
360
  createdAt: this.createdAt,
293
361
  completedAt: this.completedAt,
362
+ worktree: this._worktreePath
363
+ ? { path: this._worktreePath, branch: this._worktreeBranch }
364
+ : null,
294
365
  };
295
366
  }
296
367
  }
@@ -290,6 +290,12 @@ export class ChainlessChainWSServer extends EventEmitter {
290
290
  case "orchestrate":
291
291
  this._handleOrchestrate(id, ws, message);
292
292
  break;
293
+ case "tasks-list":
294
+ this._handleTasksList(id, ws);
295
+ break;
296
+ case "tasks-stop":
297
+ this._handleTasksStop(id, ws, message);
298
+ break;
293
299
  default:
294
300
  this._send(ws, {
295
301
  id,
@@ -384,6 +390,45 @@ export class ChainlessChainWSServer extends EventEmitter {
384
390
  }
385
391
  }
386
392
 
393
+ /** @private — list background tasks */
394
+ _handleTasksList(id, ws) {
395
+ try {
396
+ const { BackgroundTaskManager } = require("./background-task-manager.js");
397
+ // Reuse singleton or create lightweight instance for listing
398
+ if (!this._taskManager) {
399
+ this._taskManager = new BackgroundTaskManager();
400
+ }
401
+ const tasks = this._taskManager.list();
402
+ this._send(ws, { id, type: "tasks-list", tasks });
403
+ } catch (err) {
404
+ this._send(ws, { id, type: "tasks-list", tasks: [] });
405
+ }
406
+ }
407
+
408
+ /** @private — stop a background task */
409
+ _handleTasksStop(id, ws, message) {
410
+ try {
411
+ if (this._taskManager && message.taskId) {
412
+ this._taskManager.stop(message.taskId);
413
+ this._send(ws, { id, type: "tasks-stopped", taskId: message.taskId });
414
+ } else {
415
+ this._send(ws, {
416
+ id,
417
+ type: "error",
418
+ code: "NO_TASK",
419
+ message: "taskId required or no task manager",
420
+ });
421
+ }
422
+ } catch (err) {
423
+ this._send(ws, {
424
+ id,
425
+ type: "error",
426
+ code: "TASKS_STOP_FAILED",
427
+ message: err.message,
428
+ });
429
+ }
430
+ }
431
+
387
432
  /** @private */
388
433
  _handleAuth(clientId, ws, message) {
389
434
  const { id, token } = message;
@@ -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";
@@ -108,9 +116,9 @@ export async function startAgentRepl(options = {}) {
108
116
  // Continue without DB — static prompt fallback
109
117
  }
110
118
 
111
- // Initialize prompt compressor
119
+ // Initialize prompt compressor (adaptive to model's context window)
112
120
  if (feature("PROMPT_COMPRESSOR")) {
113
- _compressor = new PromptCompressor({ maxMessages: 20, maxTokens: 8000 });
121
+ _compressor = new PromptCompressor({ model, provider });
114
122
  }
115
123
 
116
124
  // Initialize permanent memory
@@ -133,7 +141,18 @@ export async function startAgentRepl(options = {}) {
133
141
  _hookDb = db;
134
142
 
135
143
  // Resume existing session or create new one
136
- if (db && options.sessionId) {
144
+ const useJsonl = feature("JSONL_SESSION");
145
+
146
+ if (useJsonl && options.sessionId) {
147
+ // JSONL resume: check if session file exists
148
+ try {
149
+ if (sessionExists(options.sessionId)) {
150
+ sessionId = options.sessionId;
151
+ }
152
+ } catch (_err) {
153
+ // Non-critical
154
+ }
155
+ } else if (db && options.sessionId) {
137
156
  try {
138
157
  const existing = getSession(db, options.sessionId);
139
158
  if (existing && existing.messages) {
@@ -144,16 +163,25 @@ export async function startAgentRepl(options = {}) {
144
163
  }
145
164
  }
146
165
 
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
166
+ if (!sessionId) {
167
+ const meta = {
168
+ title: `Agent ${new Date().toISOString().slice(0, 10)}`,
169
+ provider,
170
+ model,
171
+ };
172
+ if (useJsonl) {
173
+ try {
174
+ sessionId = jsonlStartSession(null, meta);
175
+ } catch (_err) {
176
+ // Non-critical
177
+ }
178
+ } else if (db) {
179
+ try {
180
+ const session = createSession(db, meta);
181
+ sessionId = session.id;
182
+ } catch (_err) {
183
+ // Non-critical
184
+ }
157
185
  }
158
186
  }
159
187
 
@@ -162,16 +190,28 @@ export async function startAgentRepl(options = {}) {
162
190
  ];
163
191
 
164
192
  // Load resumed session messages
165
- if (db && options.sessionId && sessionId) {
193
+ if (options.sessionId && sessionId) {
166
194
  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)`);
195
+ if (useJsonl) {
196
+ const rebuilt = rebuildMessages(sessionId);
197
+ if (rebuilt.length > 0) {
198
+ messages.push(...rebuilt.filter((m) => m.role !== "system"));
199
+ logger.info(
200
+ `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
201
+ );
202
+ }
203
+ } else if (db) {
204
+ const existing = getSession(db, sessionId);
205
+ if (existing && existing.messages) {
206
+ const parsed =
207
+ typeof existing.messages === "string"
208
+ ? JSON.parse(existing.messages)
209
+ : existing.messages;
210
+ messages.push(...parsed.filter((m) => m.role !== "system"));
211
+ logger.info(
212
+ `Resumed session ${sessionId} (${parsed.length} messages)`,
213
+ );
214
+ }
175
215
  }
176
216
  } catch (_err) {
177
217
  // Non-critical
@@ -434,10 +474,16 @@ export async function startAgentRepl(options = {}) {
434
474
  const sessionArg = trimmed.slice(8).trim();
435
475
  if (sessionArg.startsWith("resume ")) {
436
476
  const resumeId = sessionArg.slice(7).trim();
437
- if (!db) {
438
- logger.info("No database available for session resume");
439
- } else {
440
- try {
477
+ try {
478
+ if (useJsonl && sessionExists(resumeId)) {
479
+ const rebuilt = rebuildMessages(resumeId);
480
+ messages.length = 1; // keep system prompt
481
+ messages.push(...rebuilt.filter((m) => m.role !== "system"));
482
+ sessionId = resumeId;
483
+ logger.info(
484
+ `Resumed JSONL session ${sessionId} (${rebuilt.length} messages)`,
485
+ );
486
+ } else if (db) {
441
487
  const existing = getSession(db, resumeId);
442
488
  if (existing && existing.messages) {
443
489
  const parsed =
@@ -453,9 +499,11 @@ export async function startAgentRepl(options = {}) {
453
499
  } else {
454
500
  logger.info(`Session not found: ${resumeId}`);
455
501
  }
456
- } catch (err) {
457
- logger.error(`Resume failed: ${err.message}`);
502
+ } else {
503
+ logger.info("No session store available");
458
504
  }
505
+ } catch (err) {
506
+ logger.error(`Resume failed: ${err.message}`);
459
507
  }
460
508
  } else {
461
509
  logger.info(`Session ID: ${sessionId || "none"}`);
@@ -1052,9 +1100,17 @@ export async function startAgentRepl(options = {}) {
1052
1100
  }
1053
1101
 
1054
1102
  // Auto-save session
1055
- if (db && sessionId) {
1103
+ if (sessionId) {
1056
1104
  try {
1057
- saveMessages(db, sessionId, messages);
1105
+ if (useJsonl) {
1106
+ // Append incremental events (user + assistant)
1107
+ appendUserMessage(sessionId, trimmed);
1108
+ if (response) {
1109
+ appendAssistantMessage(sessionId, response);
1110
+ }
1111
+ } else if (db) {
1112
+ saveMessages(db, sessionId, messages);
1113
+ }
1058
1114
  } catch (_e) {
1059
1115
  // Non-critical
1060
1116
  }
@@ -1074,6 +1130,13 @@ export async function startAgentRepl(options = {}) {
1074
1130
  logger.verbose(
1075
1131
  `Auto-compacted: ${stats.strategy} (saved ${stats.saved} tokens)`,
1076
1132
  );
1133
+ // Write compact checkpoint to JSONL for crash recovery
1134
+ if (useJsonl && sessionId) {
1135
+ appendCompactEvent(sessionId, {
1136
+ ...stats,
1137
+ messages: compacted,
1138
+ });
1139
+ }
1077
1140
  }
1078
1141
  } catch (_e) {
1079
1142
  // Non-critical — continue with uncompacted messages
@@ -1116,9 +1179,17 @@ export async function startAgentRepl(options = {}) {
1116
1179
 
1117
1180
  rl.on("close", async () => {
1118
1181
  // Save session on exit
1119
- if (db && sessionId) {
1182
+ if (sessionId) {
1120
1183
  try {
1121
- saveMessages(db, sessionId, messages);
1184
+ if (useJsonl) {
1185
+ // JSONL: write final compact snapshot for fast rebuild
1186
+ appendCompactEvent(sessionId, {
1187
+ strategy: "session-end",
1188
+ messages,
1189
+ });
1190
+ } else if (db) {
1191
+ saveMessages(db, sessionId, messages);
1192
+ }
1122
1193
  } catch (_e) {
1123
1194
  // Non-critical
1124
1195
  }