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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/AppLayout-BfLjLMsm.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-CFP4dGIJ.css +1 -0
- package/src/assets/web-panel/assets/{Chat-5f__rMCR.js → Chat-DP7PO9Li.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-DyQF-7R1.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-DsjXpZor.js → Dashboard-BGGdnr6t.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-BOii-AoO.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-DmiJtJYr.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-CDRMMobU.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-CVhqqoS1.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dkt7021l.js} +1 -1
- package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-DUDL_UGb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-DXXELJc3.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +1 -0
- package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +1 -0
- package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-vW799KpE.js} +2 -2
- package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/session.js +84 -18
- package/src/lib/prompt-compressor.js +132 -2
- package/src/lib/sub-agent-context.js +71 -0
- package/src/lib/ws-server.js +45 -0
- package/src/repl/agent-repl.js +103 -32
- package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
- 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-
|
|
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-
|
|
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>
|
package/src/commands/session.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
}
|
package/src/lib/ws-server.js
CHANGED
|
@@ -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;
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 (
|
|
193
|
+
if (options.sessionId && sessionId) {
|
|
166
194
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
}
|
|
457
|
-
logger.
|
|
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 (
|
|
1103
|
+
if (sessionId) {
|
|
1056
1104
|
try {
|
|
1057
|
-
|
|
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 (
|
|
1182
|
+
if (sessionId) {
|
|
1120
1183
|
try {
|
|
1121
|
-
|
|
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
|
}
|