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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/AppLayout-B00RARl2.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-DXtvKoM0.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-C4mrNC4c.js → Cron-BJ4ODHOy.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Logs-CC_Zuh66.js → Logs-CSeKZEG_.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-B15GiN3u.js → McpTools-BYQAK11r.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-Dbd7oLOH.js → Memory-gkUAPyuZ.js} +2 -2
- package/src/assets/web-panel/assets/{Notes-CEkc49fY.js → Notes-bjNrQgAo.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CjyPHW00.js → Providers-Dbf57Tbv.js} +1 -1
- package/src/assets/web-panel/assets/{Services-XFzHMRRd.js → Services-CS0oMdxh.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D8oxmB3U.js → Skills-B2fgruv8.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
- package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
- package/src/assets/web-panel/assets/{antd-ChLPLhSn.js → antd-CJSBocer.js} +1 -1
- package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
- package/src/assets/web-panel/assets/{index-DQ5xXK7O.js → index-CF2CqPYX.js} +2 -2
- package/src/assets/web-panel/assets/{markdown-DtbPhnFe.js → markdown-Bo5cVN4u.js} +1 -1
- package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent.js +7 -8
- package/src/commands/chat.js +9 -11
- package/src/commands/serve.js +11 -106
- package/src/commands/session.js +185 -18
- package/src/commands/ui.js +10 -151
- package/src/gateways/repl/agent-repl.js +1 -0
- package/src/gateways/repl/chat-repl.js +1 -0
- package/src/gateways/ui/web-ui-server.js +1 -0
- package/src/gateways/ws/action-protocol.js +83 -0
- package/src/gateways/ws/message-dispatcher.js +73 -0
- package/src/gateways/ws/session-protocol.js +396 -0
- package/src/gateways/ws/task-protocol.js +55 -0
- package/src/gateways/ws/worktree-protocol.js +315 -0
- package/src/gateways/ws/ws-server.js +4 -0
- package/src/gateways/ws/ws-session-gateway.js +1 -0
- package/src/harness/background-task-manager.js +506 -0
- package/src/harness/background-task-worker.js +48 -0
- package/src/harness/compression-telemetry.js +214 -0
- package/src/harness/feature-flags.js +157 -0
- package/src/harness/jsonl-session-store.js +452 -0
- package/src/harness/prompt-compressor.js +416 -0
- package/src/harness/worktree-isolator.js +845 -0
- package/src/lib/agent-core.js +246 -45
- package/src/lib/background-task-manager.js +1 -305
- package/src/lib/background-task-worker.js +1 -50
- package/src/lib/compression-telemetry.js +5 -0
- package/src/lib/feature-flags.js +7 -182
- package/src/lib/interaction-adapter.js +32 -6
- package/src/lib/jsonl-session-store.js +21 -237
- package/src/lib/prompt-compressor.js +10 -351
- package/src/lib/sub-agent-context.js +91 -0
- package/src/lib/worktree-isolator.js +13 -231
- package/src/lib/ws-agent-handler.js +1 -0
- package/src/lib/ws-server.js +155 -359
- package/src/lib/ws-session-manager.js +82 -1
- package/src/repl/agent-repl.js +114 -32
- package/src/runtime/agent-runtime.js +417 -0
- package/src/runtime/contracts/agent-turn.js +11 -0
- package/src/runtime/contracts/session-record.js +31 -0
- package/src/runtime/contracts/task-record.js +18 -0
- package/src/runtime/contracts/telemetry-record.js +23 -0
- package/src/runtime/contracts/worktree-record.js +14 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/policies/agent-policy.js +45 -0
- package/src/runtime/runtime-context.js +14 -0
- package/src/runtime/runtime-events.js +37 -0
- package/src/runtime/runtime-factory.js +50 -0
- package/src/tools/index.js +22 -0
- package/src/tools/legacy-agent-tools.js +171 -0
- package/src/tools/registry.js +141 -0
- package/src/tools/tool-context.js +28 -0
- package/src/tools/tool-permissions.js +28 -0
- package/src/tools/tool-telemetry.js +39 -0
- package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
- package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +0 -3
- package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
- package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
|
@@ -11,6 +11,16 @@
|
|
|
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
|
+
diffWorktree,
|
|
20
|
+
mergeWorktree,
|
|
21
|
+
worktreeLog,
|
|
22
|
+
} from "./worktree-isolator.js";
|
|
23
|
+
import { isGitRepo } from "./git-integration.js";
|
|
14
24
|
|
|
15
25
|
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
16
26
|
|
|
@@ -38,6 +48,7 @@ export class SubAgentContext {
|
|
|
38
48
|
* @param {object} [options.permanentMemory] - Permanent memory instance
|
|
39
49
|
* @param {object} [options.llmOptions] - LLM provider/model/key options
|
|
40
50
|
* @param {string} [options.cwd] - Working directory
|
|
51
|
+
* @param {boolean} [options.useWorktree] - Force worktree isolation (overrides flag)
|
|
41
52
|
* @returns {SubAgentContext}
|
|
42
53
|
*/
|
|
43
54
|
static create(options = {}) {
|
|
@@ -59,6 +70,12 @@ export class SubAgentContext {
|
|
|
59
70
|
this.createdAt = new Date().toISOString();
|
|
60
71
|
this.completedAt = null;
|
|
61
72
|
|
|
73
|
+
// Worktree isolation state
|
|
74
|
+
this._useWorktree = options.useWorktree ?? feature("WORKTREE_ISOLATION");
|
|
75
|
+
this._worktreePath = null;
|
|
76
|
+
this._worktreeBranch = null;
|
|
77
|
+
this._repoDir = this.cwd;
|
|
78
|
+
|
|
62
79
|
// ── Isolated state ──────────────────────────────────────────────
|
|
63
80
|
// Independent message history — never shared with parent
|
|
64
81
|
this.messages = [];
|
|
@@ -110,6 +127,77 @@ export class SubAgentContext {
|
|
|
110
127
|
);
|
|
111
128
|
}
|
|
112
129
|
|
|
130
|
+
// If worktree isolation is enabled, wrap execution in isolated worktree
|
|
131
|
+
if (this._useWorktree && isGitRepo(this._repoDir)) {
|
|
132
|
+
return this._runInWorktree(userPrompt, loopOptions);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this._runCore(userPrompt, loopOptions);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run in an isolated git worktree. Creates worktree → runs → cleans up.
|
|
140
|
+
*/
|
|
141
|
+
async _runInWorktree(userPrompt, loopOptions = {}) {
|
|
142
|
+
const taskId = `${this.role}-${this.id.slice(4)}`;
|
|
143
|
+
try {
|
|
144
|
+
const { result, branch, worktreePath, hasChanges } = await isolateTask(
|
|
145
|
+
this._repoDir,
|
|
146
|
+
taskId,
|
|
147
|
+
async (wtPath) => {
|
|
148
|
+
this._worktreePath = wtPath;
|
|
149
|
+
this._worktreeBranch = `agent/${taskId}`;
|
|
150
|
+
// Override cwd to worktree for tool execution
|
|
151
|
+
this.cwd = wtPath;
|
|
152
|
+
return this._runCore(userPrompt, loopOptions);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Annotate result with worktree info + diff preview
|
|
157
|
+
if (result) {
|
|
158
|
+
let diffInfo = null;
|
|
159
|
+
let commits = [];
|
|
160
|
+
if (
|
|
161
|
+
hasChanges ||
|
|
162
|
+
worktreeLog(this._repoDir, `agent/${taskId}`).length > 0
|
|
163
|
+
) {
|
|
164
|
+
try {
|
|
165
|
+
diffInfo = diffWorktree(this._repoDir, `agent/${taskId}`);
|
|
166
|
+
commits = worktreeLog(this._repoDir, `agent/${taskId}`);
|
|
167
|
+
} catch (_e) {
|
|
168
|
+
// Non-critical — diff preview is optional
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
result.worktree = {
|
|
172
|
+
branch,
|
|
173
|
+
path: worktreePath,
|
|
174
|
+
hasChanges,
|
|
175
|
+
diff: diffInfo,
|
|
176
|
+
commits,
|
|
177
|
+
merge: (options = {}) =>
|
|
178
|
+
mergeWorktree(this._repoDir, branch, options),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
// If worktree creation fails (e.g. not a git repo), fall back to direct
|
|
184
|
+
this.status = "failed";
|
|
185
|
+
this.completedAt = new Date().toISOString();
|
|
186
|
+
this.result = {
|
|
187
|
+
summary: `Worktree isolation failed: ${err.message}`,
|
|
188
|
+
artifacts: [],
|
|
189
|
+
tokenCount: this._tokenCount,
|
|
190
|
+
toolsUsed: [...new Set(this._toolsUsed)],
|
|
191
|
+
iterationCount: this._iterationCount,
|
|
192
|
+
};
|
|
193
|
+
return this.result;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Core agent loop execution (shared by direct and worktree paths).
|
|
199
|
+
*/
|
|
200
|
+
async _runCore(userPrompt, loopOptions = {}) {
|
|
113
201
|
// Add user message
|
|
114
202
|
this.messages.push({ role: "user", content: userPrompt });
|
|
115
203
|
|
|
@@ -291,6 +379,9 @@ export class SubAgentContext {
|
|
|
291
379
|
iterationCount: this._iterationCount,
|
|
292
380
|
createdAt: this.createdAt,
|
|
293
381
|
completedAt: this.completedAt,
|
|
382
|
+
worktree: this._worktreePath
|
|
383
|
+
? { path: this._worktreePath, branch: this._worktreeBranch }
|
|
384
|
+
: null,
|
|
294
385
|
};
|
|
295
386
|
}
|
|
296
387
|
}
|
|
@@ -1,231 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const WORKTREE_DIR = ".worktrees";
|
|
16
|
-
|
|
17
|
-
// ── Low-level worktree operations ───────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a new git worktree with a new branch.
|
|
21
|
-
* @param {string} repoDir — Root of the git repository
|
|
22
|
-
* @param {string} branchName — Branch name (e.g. "agent/task-123")
|
|
23
|
-
* @param {string} [baseBranch] — Base branch (default: HEAD)
|
|
24
|
-
* @returns {{ path: string, branch: string }}
|
|
25
|
-
*/
|
|
26
|
-
export function createWorktree(repoDir, branchName, baseBranch) {
|
|
27
|
-
if (!isGitRepo(repoDir)) {
|
|
28
|
-
throw new Error("Not a git repository");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const worktreePath = resolve(
|
|
32
|
-
repoDir,
|
|
33
|
-
WORKTREE_DIR,
|
|
34
|
-
branchName.replace(/\//g, "-"),
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
if (existsSync(worktreePath)) {
|
|
38
|
-
throw new Error(`Worktree already exists: ${worktreePath}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const base = baseBranch || "HEAD";
|
|
42
|
-
gitExec(`worktree add "${worktreePath}" -b "${branchName}" ${base}`, repoDir);
|
|
43
|
-
|
|
44
|
-
return { path: worktreePath, branch: branchName };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Remove a git worktree and its branch.
|
|
49
|
-
* @param {string} repoDir
|
|
50
|
-
* @param {string} worktreePath — Absolute path to the worktree
|
|
51
|
-
* @param {object} [options]
|
|
52
|
-
* @param {boolean} [options.deleteBranch=true] — Also delete the branch
|
|
53
|
-
*/
|
|
54
|
-
export function removeWorktree(repoDir, worktreePath, options = {}) {
|
|
55
|
-
const deleteBranch = options.deleteBranch !== false;
|
|
56
|
-
|
|
57
|
-
// Get branch name before removal
|
|
58
|
-
let branch = null;
|
|
59
|
-
if (deleteBranch) {
|
|
60
|
-
try {
|
|
61
|
-
branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
62
|
-
cwd: worktreePath,
|
|
63
|
-
encoding: "utf-8",
|
|
64
|
-
}).trim();
|
|
65
|
-
} catch (_e) {
|
|
66
|
-
// Can't determine branch — skip branch deletion
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
gitExec(`worktree remove "${worktreePath}" --force`, repoDir);
|
|
72
|
-
} catch (_e) {
|
|
73
|
-
// If git worktree remove fails, try manual cleanup
|
|
74
|
-
if (existsSync(worktreePath)) {
|
|
75
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
76
|
-
}
|
|
77
|
-
gitExec("worktree prune", repoDir);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Delete the branch
|
|
81
|
-
if (
|
|
82
|
-
deleteBranch &&
|
|
83
|
-
branch &&
|
|
84
|
-
branch !== "HEAD" &&
|
|
85
|
-
!branch.startsWith("main") &&
|
|
86
|
-
!branch.startsWith("master")
|
|
87
|
-
) {
|
|
88
|
-
try {
|
|
89
|
-
gitExec(`branch -D "${branch}"`, repoDir);
|
|
90
|
-
} catch (_e) {
|
|
91
|
-
// Branch might already be deleted
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* List all worktrees.
|
|
98
|
-
* @param {string} repoDir
|
|
99
|
-
* @returns {Array<{path: string, branch: string, head: string}>}
|
|
100
|
-
*/
|
|
101
|
-
export function listWorktrees(repoDir) {
|
|
102
|
-
if (!isGitRepo(repoDir)) return [];
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const output = gitExec("worktree list --porcelain", repoDir);
|
|
106
|
-
const worktrees = [];
|
|
107
|
-
let current = {};
|
|
108
|
-
|
|
109
|
-
for (const line of output.split("\n")) {
|
|
110
|
-
if (line.startsWith("worktree ")) {
|
|
111
|
-
if (current.path) worktrees.push(current);
|
|
112
|
-
current = { path: line.slice(9) };
|
|
113
|
-
} else if (line.startsWith("HEAD ")) {
|
|
114
|
-
current.head = line.slice(5);
|
|
115
|
-
} else if (line.startsWith("branch ")) {
|
|
116
|
-
current.branch = line.slice(7).replace("refs/heads/", "");
|
|
117
|
-
} else if (line === "bare") {
|
|
118
|
-
current.bare = true;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (current.path) worktrees.push(current);
|
|
122
|
-
|
|
123
|
-
return worktrees;
|
|
124
|
-
} catch (_e) {
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Prune stale worktrees (where directory no longer exists).
|
|
131
|
-
* @param {string} repoDir
|
|
132
|
-
* @returns {number} Number pruned
|
|
133
|
-
*/
|
|
134
|
-
export function pruneWorktrees(repoDir) {
|
|
135
|
-
if (!isGitRepo(repoDir)) return 0;
|
|
136
|
-
|
|
137
|
-
const before = listWorktrees(repoDir).length;
|
|
138
|
-
gitExec("worktree prune", repoDir);
|
|
139
|
-
const after = listWorktrees(repoDir).length;
|
|
140
|
-
return before - after;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── High-level isolation API ────────────────────────────────────────────
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Run a function in an isolated git worktree.
|
|
147
|
-
*
|
|
148
|
-
* Creates a worktree → executes fn(worktreePath) → cleans up.
|
|
149
|
-
* If fn throws, the worktree is still cleaned up.
|
|
150
|
-
*
|
|
151
|
-
* @param {string} repoDir — Root of the git repository
|
|
152
|
-
* @param {string} taskId — Used to generate branch name: agent/{taskId}
|
|
153
|
-
* @param {Function} fn — async (worktreePath) => result
|
|
154
|
-
* @returns {Promise<{result, branch, merged}>}
|
|
155
|
-
*/
|
|
156
|
-
export async function isolateTask(repoDir, taskId, fn) {
|
|
157
|
-
const branchName = `agent/${taskId}`;
|
|
158
|
-
const { path: worktreePath } = createWorktree(repoDir, branchName);
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const result = await fn(worktreePath);
|
|
162
|
-
|
|
163
|
-
// Check if the worktree has any changes
|
|
164
|
-
const hasChanges = _hasUncommittedChanges(worktreePath);
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
result,
|
|
168
|
-
branch: branchName,
|
|
169
|
-
worktreePath,
|
|
170
|
-
hasChanges,
|
|
171
|
-
};
|
|
172
|
-
} finally {
|
|
173
|
-
// Always clean up the worktree (but keep branch if there are commits)
|
|
174
|
-
try {
|
|
175
|
-
const hasCommits = _hasBranchCommits(repoDir, branchName);
|
|
176
|
-
removeWorktree(repoDir, worktreePath, {
|
|
177
|
-
deleteBranch: !hasCommits,
|
|
178
|
-
});
|
|
179
|
-
} catch (_e) {
|
|
180
|
-
// Best-effort cleanup
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Clean up all agent worktrees (e.g. after crash recovery).
|
|
187
|
-
* @param {string} repoDir
|
|
188
|
-
* @returns {number} Number of worktrees cleaned
|
|
189
|
-
*/
|
|
190
|
-
export function cleanupAgentWorktrees(repoDir) {
|
|
191
|
-
const worktrees = listWorktrees(repoDir);
|
|
192
|
-
let cleaned = 0;
|
|
193
|
-
|
|
194
|
-
for (const wt of worktrees) {
|
|
195
|
-
if (wt.branch && wt.branch.startsWith("agent/")) {
|
|
196
|
-
try {
|
|
197
|
-
removeWorktree(repoDir, wt.path, { deleteBranch: true });
|
|
198
|
-
cleaned++;
|
|
199
|
-
} catch (_e) {
|
|
200
|
-
// Skip if can't clean
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
pruneWorktrees(repoDir);
|
|
206
|
-
return cleaned;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── Internal helpers ────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
function _hasUncommittedChanges(worktreePath) {
|
|
212
|
-
try {
|
|
213
|
-
const output = execSync("git status --porcelain", {
|
|
214
|
-
cwd: worktreePath,
|
|
215
|
-
encoding: "utf-8",
|
|
216
|
-
});
|
|
217
|
-
return output.trim().length > 0;
|
|
218
|
-
} catch (_e) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function _hasBranchCommits(repoDir, branchName) {
|
|
224
|
-
try {
|
|
225
|
-
// Check if branch has commits not in HEAD
|
|
226
|
-
const output = gitExec(`log HEAD..${branchName} --oneline`, repoDir);
|
|
227
|
-
return output.trim().length > 0;
|
|
228
|
-
} catch (_e) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
createWorktree,
|
|
3
|
+
removeWorktree,
|
|
4
|
+
listWorktrees,
|
|
5
|
+
pruneWorktrees,
|
|
6
|
+
isolateTask,
|
|
7
|
+
cleanupAgentWorktrees,
|
|
8
|
+
diffWorktree,
|
|
9
|
+
previewWorktreeMerge,
|
|
10
|
+
applyWorktreeAutomationCandidate,
|
|
11
|
+
mergeWorktree,
|
|
12
|
+
worktreeLog,
|
|
13
|
+
} from "../harness/worktree-isolator.js";
|