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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { getHomeDir } from "../lib/paths.js";
|
|
10
|
+
import { createTelemetryRecord } from "../runtime/contracts/telemetry-record.js";
|
|
11
|
+
|
|
12
|
+
function getMetricsDir() {
|
|
13
|
+
const dir = join(getHomeDir(), "metrics");
|
|
14
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function telemetryPath() {
|
|
19
|
+
return join(getMetricsDir(), "compression.jsonl");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function recordCompressionMetric(stats, meta = {}) {
|
|
23
|
+
if (!stats || typeof stats !== "object") return;
|
|
24
|
+
|
|
25
|
+
const record = createTelemetryRecord(
|
|
26
|
+
{
|
|
27
|
+
...stats,
|
|
28
|
+
provider: meta.provider || null,
|
|
29
|
+
model: meta.model || null,
|
|
30
|
+
source: meta.source || null,
|
|
31
|
+
},
|
|
32
|
+
meta,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const event = {
|
|
36
|
+
timestamp: record.timestamp,
|
|
37
|
+
stats,
|
|
38
|
+
meta,
|
|
39
|
+
record,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
appendFileSync(telemetryPath(), JSON.stringify(event) + "\n", "utf-8");
|
|
44
|
+
} catch (_err) {
|
|
45
|
+
// Non-critical
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getCompressionTelemetrySummary(options = {}) {
|
|
50
|
+
const filePath = telemetryPath();
|
|
51
|
+
const limit = options.limit || 500;
|
|
52
|
+
const windowMs = options.windowMs || null;
|
|
53
|
+
const provider = options.provider || null;
|
|
54
|
+
const model = options.model || null;
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (!existsSync(filePath)) {
|
|
57
|
+
return emptyCompressionSummary();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines = readFileSync(filePath, "utf-8")
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter((line) => line.trim())
|
|
63
|
+
.slice(-limit);
|
|
64
|
+
|
|
65
|
+
const samples = [];
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(line);
|
|
69
|
+
if (!parsed?.stats) continue;
|
|
70
|
+
const record = parsed.record || null;
|
|
71
|
+
const recordTimestamp = record?.timestamp || parsed.timestamp || 0;
|
|
72
|
+
const recordProvider = record?.provider || parsed?.meta?.provider || null;
|
|
73
|
+
const recordModel = record?.model || parsed?.meta?.model || null;
|
|
74
|
+
if (windowMs && now - recordTimestamp > windowMs) continue;
|
|
75
|
+
if (provider && recordProvider !== provider) continue;
|
|
76
|
+
if (model && recordModel !== model) continue;
|
|
77
|
+
samples.push(parsed);
|
|
78
|
+
} catch {
|
|
79
|
+
// Skip malformed lines
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (samples.length === 0) {
|
|
84
|
+
return emptyCompressionSummary();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const variantDistribution = {};
|
|
88
|
+
const strategyMap = new Map();
|
|
89
|
+
const providerMap = new Map();
|
|
90
|
+
const modelMap = new Map();
|
|
91
|
+
let totalSavedTokens = 0;
|
|
92
|
+
let totalOriginalTokens = 0;
|
|
93
|
+
let totalCompressedTokens = 0;
|
|
94
|
+
let compressedSamples = 0;
|
|
95
|
+
|
|
96
|
+
for (const sample of samples) {
|
|
97
|
+
const stats = sample.stats || {};
|
|
98
|
+
totalSavedTokens += stats.saved || 0;
|
|
99
|
+
totalOriginalTokens += stats.originalTokens || 0;
|
|
100
|
+
totalCompressedTokens += stats.compressedTokens || 0;
|
|
101
|
+
if ((stats.saved || 0) > 0 || stats.strategy !== "none") {
|
|
102
|
+
compressedSamples++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const variant = stats.abVariant || "default";
|
|
106
|
+
variantDistribution[variant] = (variantDistribution[variant] || 0) + 1;
|
|
107
|
+
|
|
108
|
+
const strategies = String(stats.strategy || "none").split("+");
|
|
109
|
+
for (const strategy of strategies) {
|
|
110
|
+
const key = strategy || "none";
|
|
111
|
+
const current = strategyMap.get(key) || {
|
|
112
|
+
strategy: key,
|
|
113
|
+
hits: 0,
|
|
114
|
+
savedTokens: 0,
|
|
115
|
+
};
|
|
116
|
+
current.hits += 1;
|
|
117
|
+
current.savedTokens += stats.saved || 0;
|
|
118
|
+
strategyMap.set(key, current);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const providerKey = sample?.meta?.provider || "unknown";
|
|
122
|
+
const modelKey = sample?.meta?.model || "unknown";
|
|
123
|
+
accumulateSlice(providerMap, providerKey, stats);
|
|
124
|
+
accumulateSlice(modelMap, modelKey, stats);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const strategyDistribution = [...strategyMap.values()]
|
|
128
|
+
.map((entry) => ({
|
|
129
|
+
...entry,
|
|
130
|
+
hitRate: samples.length > 0 ? entry.hits / samples.length : 0,
|
|
131
|
+
}))
|
|
132
|
+
.sort((a, b) => b.hits - a.hits);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
samples: samples.length,
|
|
136
|
+
compressedSamples,
|
|
137
|
+
hitRate: samples.length > 0 ? compressedSamples / samples.length : 0,
|
|
138
|
+
totalSavedTokens,
|
|
139
|
+
averageSavedTokens:
|
|
140
|
+
samples.length > 0 ? Math.round(totalSavedTokens / samples.length) : 0,
|
|
141
|
+
totalOriginalTokens,
|
|
142
|
+
totalCompressedTokens,
|
|
143
|
+
netSavingsRate:
|
|
144
|
+
totalOriginalTokens > 0
|
|
145
|
+
? (totalOriginalTokens - totalCompressedTokens) / totalOriginalTokens
|
|
146
|
+
: 0,
|
|
147
|
+
filters: {
|
|
148
|
+
limit,
|
|
149
|
+
windowMs,
|
|
150
|
+
provider,
|
|
151
|
+
model,
|
|
152
|
+
},
|
|
153
|
+
variantDistribution,
|
|
154
|
+
strategyDistribution,
|
|
155
|
+
providerDistribution: finalizeSliceDistribution(providerMap, samples.length),
|
|
156
|
+
modelDistribution: finalizeSliceDistribution(modelMap, samples.length),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resetCompressionTelemetry() {
|
|
161
|
+
try {
|
|
162
|
+
rmSync(telemetryPath(), { force: true });
|
|
163
|
+
} catch (_err) {
|
|
164
|
+
// Non-critical
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function emptyCompressionSummary() {
|
|
169
|
+
return {
|
|
170
|
+
samples: 0,
|
|
171
|
+
compressedSamples: 0,
|
|
172
|
+
hitRate: 0,
|
|
173
|
+
totalSavedTokens: 0,
|
|
174
|
+
averageSavedTokens: 0,
|
|
175
|
+
totalOriginalTokens: 0,
|
|
176
|
+
totalCompressedTokens: 0,
|
|
177
|
+
netSavingsRate: 0,
|
|
178
|
+
filters: {
|
|
179
|
+
limit: 500,
|
|
180
|
+
windowMs: null,
|
|
181
|
+
provider: null,
|
|
182
|
+
model: null,
|
|
183
|
+
},
|
|
184
|
+
variantDistribution: {},
|
|
185
|
+
strategyDistribution: [],
|
|
186
|
+
providerDistribution: [],
|
|
187
|
+
modelDistribution: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function accumulateSlice(map, key, stats) {
|
|
192
|
+
const current = map.get(key) || {
|
|
193
|
+
key,
|
|
194
|
+
samples: 0,
|
|
195
|
+
compressedSamples: 0,
|
|
196
|
+
savedTokens: 0,
|
|
197
|
+
};
|
|
198
|
+
current.samples += 1;
|
|
199
|
+
current.savedTokens += stats.saved || 0;
|
|
200
|
+
if ((stats.saved || 0) > 0 || stats.strategy !== "none") {
|
|
201
|
+
current.compressedSamples += 1;
|
|
202
|
+
}
|
|
203
|
+
map.set(key, current);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function finalizeSliceDistribution(map, totalSamples) {
|
|
207
|
+
return [...map.values()]
|
|
208
|
+
.map((entry) => ({
|
|
209
|
+
...entry,
|
|
210
|
+
hitRate: entry.samples > 0 ? entry.compressedSamples / entry.samples : 0,
|
|
211
|
+
share: totalSamples > 0 ? entry.samples / totalSamples : 0,
|
|
212
|
+
}))
|
|
213
|
+
.sort((a, b) => b.samples - a.samples);
|
|
214
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Flag System — runtime feature gating for gradual rollout.
|
|
3
|
+
*
|
|
4
|
+
* Flags are stored in .chainlesschain/config.json under "features" key.
|
|
5
|
+
* Each flag can be:
|
|
6
|
+
* - boolean (true/false)
|
|
7
|
+
* - number 0-100 (percentage rollout, hashed by machine-id)
|
|
8
|
+
* - object { enabled, variant, description }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { loadConfig, getConfigValue, saveConfig } from "../lib/config-manager.js";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { hostname } from "node:os";
|
|
14
|
+
|
|
15
|
+
const FLAG_REGISTRY = {
|
|
16
|
+
BACKGROUND_TASKS: {
|
|
17
|
+
description: "Enable background task queue with daemon execution",
|
|
18
|
+
default: false,
|
|
19
|
+
},
|
|
20
|
+
WORKTREE_ISOLATION: {
|
|
21
|
+
description: "Enable git worktree isolation for agent tasks",
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
CONTEXT_SNIP: {
|
|
25
|
+
description: "Enable snipCompact strategy in context compression",
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
CONTEXT_COLLAPSE: {
|
|
29
|
+
description: "Enable contextCollapse strategy in context compression",
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
JSONL_SESSION: {
|
|
33
|
+
description: "Use JSONL append-only format for session persistence",
|
|
34
|
+
default: true,
|
|
35
|
+
},
|
|
36
|
+
PROMPT_COMPRESSOR: {
|
|
37
|
+
description: "Enable CLI prompt compressor (auto/snip/collapse)",
|
|
38
|
+
default: true,
|
|
39
|
+
},
|
|
40
|
+
COMPRESSION_AB: {
|
|
41
|
+
description:
|
|
42
|
+
"A/B test compression thresholds (variants: aggressive, balanced, relaxed)",
|
|
43
|
+
default: { enabled: false, variant: "balanced" },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function feature(name) {
|
|
48
|
+
const value = _resolve(name);
|
|
49
|
+
if (typeof value === "boolean") return value;
|
|
50
|
+
if (typeof value === "number") return _percentageCheck(name, value);
|
|
51
|
+
if (value && typeof value === "object") return Boolean(value.enabled);
|
|
52
|
+
const defaultValue = _getDefault(name);
|
|
53
|
+
if (typeof defaultValue === "boolean") return defaultValue;
|
|
54
|
+
if (typeof defaultValue === "number") return _percentageCheck(name, defaultValue);
|
|
55
|
+
if (defaultValue && typeof defaultValue === "object") {
|
|
56
|
+
return Boolean(defaultValue.enabled);
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function featureVariant(name) {
|
|
62
|
+
const value = _resolve(name);
|
|
63
|
+
if (value && typeof value === "object" && value.variant) {
|
|
64
|
+
return value.variant;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function listFeatures() {
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const features = config.features || {};
|
|
72
|
+
const result = [];
|
|
73
|
+
|
|
74
|
+
for (const [name, meta] of Object.entries(FLAG_REGISTRY)) {
|
|
75
|
+
const raw = features[name];
|
|
76
|
+
const enabled = feature(name);
|
|
77
|
+
const source =
|
|
78
|
+
raw !== undefined
|
|
79
|
+
? "config"
|
|
80
|
+
: process.env[`CC_FLAG_${name}`] !== undefined
|
|
81
|
+
? "env"
|
|
82
|
+
: "default";
|
|
83
|
+
|
|
84
|
+
result.push({
|
|
85
|
+
name,
|
|
86
|
+
enabled,
|
|
87
|
+
description: meta.description,
|
|
88
|
+
source,
|
|
89
|
+
raw: raw !== undefined ? raw : meta.default,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const [name, raw] of Object.entries(features)) {
|
|
94
|
+
if (!FLAG_REGISTRY[name]) {
|
|
95
|
+
result.push({
|
|
96
|
+
name,
|
|
97
|
+
enabled: Boolean(raw),
|
|
98
|
+
description: "(user-defined)",
|
|
99
|
+
source: "config",
|
|
100
|
+
raw,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function setFeature(name, value) {
|
|
109
|
+
const config = loadConfig();
|
|
110
|
+
if (!config.features) config.features = {};
|
|
111
|
+
config.features[name] = value;
|
|
112
|
+
saveConfig(config);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getFlagInfo(name) {
|
|
116
|
+
return FLAG_REGISTRY[name] || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _resolve(name) {
|
|
120
|
+
const envKey = `CC_FLAG_${name}`;
|
|
121
|
+
if (process.env[envKey] !== undefined) {
|
|
122
|
+
const envVal = process.env[envKey];
|
|
123
|
+
if (envVal === "true" || envVal === "1") return true;
|
|
124
|
+
if (envVal === "false" || envVal === "0") return false;
|
|
125
|
+
const num = Number(envVal);
|
|
126
|
+
if (!isNaN(num)) return num;
|
|
127
|
+
return envVal;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configVal = getConfigValue(`features.${name}`);
|
|
131
|
+
if (configVal !== undefined) return configVal;
|
|
132
|
+
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _getDefault(name) {
|
|
137
|
+
const meta = FLAG_REGISTRY[name];
|
|
138
|
+
return meta ? meta.default : false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _percentageCheck(name, percentage) {
|
|
142
|
+
if (percentage <= 0) return false;
|
|
143
|
+
if (percentage >= 100) return true;
|
|
144
|
+
const hash = createHash("md5")
|
|
145
|
+
.update(`${name}:${_machineId()}`)
|
|
146
|
+
.digest("hex");
|
|
147
|
+
const bucket = parseInt(hash.slice(0, 8), 16) % 100;
|
|
148
|
+
return bucket < percentage;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let _cachedMachineId = null;
|
|
152
|
+
function _machineId() {
|
|
153
|
+
if (!_cachedMachineId) {
|
|
154
|
+
_cachedMachineId = hostname() || "unknown";
|
|
155
|
+
}
|
|
156
|
+
return _cachedMachineId;
|
|
157
|
+
}
|