sideby 0.4.0 → 0.4.1
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/dist/build.json +1 -1
- package/dist/src/cli/commands.js +2 -0
- package/dist/src/cli/format.js +1 -1
- package/dist/src/cli/json-schemas.js +1 -0
- package/dist/src/core/tuning-apply.d.ts +16 -4
- package/dist/src/core/tuning-apply.js +35 -8
- package/dist/src/core/tuning.d.ts +15 -0
- package/dist/src/core/tuning.js +23 -7
- package/dist/src/families/claude/tuning.js +18 -2
- package/dist/src/panel/i18n.d.ts +2 -0
- package/dist/src/panel/i18n.js +6 -2
- package/dist/src/panel/page-script.js +10 -3
- package/dist/src/runtime.js +5 -3
- package/package.json +1 -1
- package/schemas/tune-change.json +18 -0
- package/skills/sideby/SKILL.md +1 -1
- package/skills/sideby/references/diagnose.md +1 -1
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261013T093231-6fb759"}
|
package/dist/src/cli/commands.js
CHANGED
|
@@ -312,6 +312,8 @@ function tuneChangeLines(plan, home) {
|
|
|
312
312
|
];
|
|
313
313
|
if (plan.affected.length > 1)
|
|
314
314
|
lines.push(` Careful: this file is shared, so it also changes ${plan.affected.slice(1).join(', ')}`);
|
|
315
|
+
if (plan.overrides.length)
|
|
316
|
+
lines.push(` Careful: these models save a size of their own, which wins over this one: ${plan.overrides.map((o) => `${o.model} (${compactNumber(o.value)})`).join(', ')}`);
|
|
315
317
|
lines.push(' Sessions already running keep the old value; new ones use the new one. You can undo it.');
|
|
316
318
|
return lines;
|
|
317
319
|
}
|
package/dist/src/cli/format.js
CHANGED
|
@@ -83,7 +83,7 @@ const TUNING_REASON = {
|
|
|
83
83
|
'already-best': 'already the cheapest setting we can estimate',
|
|
84
84
|
'insufficient-calls': `too few calls to estimate (needs ${MIN_CALLS})`,
|
|
85
85
|
'no-compactions': 'no compaction recorded to estimate from',
|
|
86
|
-
'no-optimum': 'the
|
|
86
|
+
'no-optimum': 'the model window is below the lowest size Claude Code accepts (100K), so there is nothing to set',
|
|
87
87
|
'unknown-model': 'not a Claude model, so the cost weights do not apply',
|
|
88
88
|
'observe-only': 'observation only (no public default for this host)',
|
|
89
89
|
};
|
|
@@ -211,6 +211,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
211
211
|
current: Type.Union([Type.Number(), Type.Null()]),
|
|
212
212
|
next: Type.Union([Type.Number(), Type.Null()]),
|
|
213
213
|
creates: Type.Boolean(),
|
|
214
|
+
overrides: Type.Optional(Type.Array(Type.Object({ model: Str, value: Type.Number() }))),
|
|
214
215
|
reason: Type.Optional(Str),
|
|
215
216
|
})),
|
|
216
217
|
message: Type.Optional(Str),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Account, ReadContext } from '../types.ts';
|
|
2
2
|
export declare const TUNE_KEY = "autoCompactWindow";
|
|
3
|
-
export declare const MIN_WINDOW =
|
|
4
|
-
export declare const MAX_WINDOW =
|
|
3
|
+
export declare const MIN_WINDOW = 100000;
|
|
4
|
+
export declare const MAX_WINDOW = 1000000;
|
|
5
5
|
export interface TuneApplyPlan {
|
|
6
6
|
/** `ready`: will write; `unchanged`: already that value; `blocked`: `reason` says why and what to do. */
|
|
7
7
|
status: 'ready' | 'unchanged' | 'blocked';
|
|
@@ -17,8 +17,14 @@ export interface TuneApplyPlan {
|
|
|
17
17
|
next: number | null;
|
|
18
18
|
/** The file does not exist yet and will be created. */
|
|
19
19
|
creates: boolean;
|
|
20
|
+
/** Models that save a size of their own (`modelSettings`), which wins over this one for them. */
|
|
21
|
+
overrides: ModelOverride[];
|
|
20
22
|
reason?: string;
|
|
21
23
|
}
|
|
24
|
+
export interface ModelOverride {
|
|
25
|
+
model: string;
|
|
26
|
+
value: number;
|
|
27
|
+
}
|
|
22
28
|
export interface TuneUndoRecord {
|
|
23
29
|
file: string;
|
|
24
30
|
/** The value before sideby wrote, or null when the key was not there. */
|
|
@@ -47,5 +53,11 @@ export declare function undoTune(account: Account, ctx: ReadContext): Promise<{
|
|
|
47
53
|
}>;
|
|
48
54
|
/** The real file an Account's setting lives in (canonical, so Accounts sharing it get the same answer). */
|
|
49
55
|
export declare function settingsFileOf(account: Account): Promise<string>;
|
|
50
|
-
/**
|
|
51
|
-
|
|
56
|
+
/**
|
|
57
|
+
* The size in the Account's settings file now (null: the default) and the sizes models save for themselves. Reads
|
|
58
|
+
* only those `autoCompactWindow` numbers.
|
|
59
|
+
*/
|
|
60
|
+
export declare function readTuneSettings(account: Account): Promise<{
|
|
61
|
+
current: number | null;
|
|
62
|
+
overrides: ModelOverride[];
|
|
63
|
+
}>;
|
|
@@ -7,9 +7,10 @@ import { mkdir, readFile, rm, stat } from 'node:fs/promises';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { UserError } from './errors.js';
|
|
9
9
|
import { fileHash, lstatOrNull, realpathOrNull, sha256, writeFileAtomic, writeIfUnchanged, } from './fs-safe.js';
|
|
10
|
+
import { MAX_COMPACT_WINDOW, MIN_COMPACT_WINDOW } from './tuning.js';
|
|
10
11
|
export const TUNE_KEY = 'autoCompactWindow';
|
|
11
|
-
export const MIN_WINDOW =
|
|
12
|
-
export const MAX_WINDOW =
|
|
12
|
+
export const MIN_WINDOW = MIN_COMPACT_WINDOW;
|
|
13
|
+
export const MAX_WINDOW = MAX_COMPACT_WINDOW;
|
|
13
14
|
const MAX_BYTES = 5_000_000;
|
|
14
15
|
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
16
|
function checkValue(value) {
|
|
@@ -47,6 +48,19 @@ async function readObject(file) {
|
|
|
47
48
|
return { error: 'it is not a JSON object' };
|
|
48
49
|
return { text, obj: parsed };
|
|
49
50
|
}
|
|
51
|
+
/** The sizes models save for themselves under `modelSettings`: only those `autoCompactWindow` numbers are read. */
|
|
52
|
+
function overridesOf(obj) {
|
|
53
|
+
const ms = obj.modelSettings;
|
|
54
|
+
if (!isObj(ms))
|
|
55
|
+
return [];
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const [model, entry] of Object.entries(ms)) {
|
|
58
|
+
const v = isObj(entry) ? entry[TUNE_KEY] : undefined;
|
|
59
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
60
|
+
out.push({ model, value: v });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
50
64
|
/** Where the Account's settings really live, and whether `settings.json` is a link to it. */
|
|
51
65
|
async function locate(account) {
|
|
52
66
|
const path = join(account.dir, 'settings.json');
|
|
@@ -91,6 +105,7 @@ export async function planTuneApply(account, value, accounts) {
|
|
|
91
105
|
current: null,
|
|
92
106
|
next: value,
|
|
93
107
|
creates: false,
|
|
108
|
+
overrides: [],
|
|
94
109
|
};
|
|
95
110
|
if (where.broken)
|
|
96
111
|
return {
|
|
@@ -117,7 +132,13 @@ export async function planTuneApply(account, value, accounts) {
|
|
|
117
132
|
if (loc.exists && loc.file === where.file)
|
|
118
133
|
affected.push(other.ref);
|
|
119
134
|
}
|
|
120
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
affected,
|
|
138
|
+
current,
|
|
139
|
+
overrides: overridesOf(read.obj),
|
|
140
|
+
status: current === value ? 'unchanged' : 'ready',
|
|
141
|
+
};
|
|
121
142
|
}
|
|
122
143
|
/** Plans, and writes only when the plan is `ready`. */
|
|
123
144
|
export async function applyTune(account, value, accounts, ctx) {
|
|
@@ -224,14 +245,20 @@ export async function undoTune(account, ctx) {
|
|
|
224
245
|
export async function settingsFileOf(account) {
|
|
225
246
|
return (await locate(account)).file;
|
|
226
247
|
}
|
|
227
|
-
/**
|
|
228
|
-
|
|
248
|
+
/**
|
|
249
|
+
* The size in the Account's settings file now (null: the default) and the sizes models save for themselves. Reads
|
|
250
|
+
* only those `autoCompactWindow` numbers.
|
|
251
|
+
*/
|
|
252
|
+
export async function readTuneSettings(account) {
|
|
229
253
|
const where = await locate(account);
|
|
230
254
|
if (!where.exists)
|
|
231
|
-
return null;
|
|
255
|
+
return { current: null, overrides: [] };
|
|
232
256
|
const read = await readObject(where.file);
|
|
233
257
|
if ('error' in read)
|
|
234
|
-
return null;
|
|
258
|
+
return { current: null, overrides: [] };
|
|
235
259
|
const v = read.obj[TUNE_KEY];
|
|
236
|
-
return
|
|
260
|
+
return {
|
|
261
|
+
current: typeof v === 'number' && Number.isFinite(v) ? v : null,
|
|
262
|
+
overrides: overridesOf(read.obj),
|
|
263
|
+
};
|
|
237
264
|
}
|
|
@@ -7,6 +7,12 @@ export declare const WEIGHT: {
|
|
|
7
7
|
readonly write1h: 2;
|
|
8
8
|
readonly output: 5;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* The sizes Claude Code accepts for the auto-compact window: 100K to 1M tokens, never above the model's context
|
|
12
|
+
* window (https://code.claude.com/docs/en/model-config#set-the-auto-compact-window).
|
|
13
|
+
*/
|
|
14
|
+
export declare const MIN_COMPACT_WINDOW = 100000;
|
|
15
|
+
export declare const MAX_COMPACT_WINDOW = 1000000;
|
|
10
16
|
/** A model with fewer turns than this in the window gets no suggestion. */
|
|
11
17
|
export declare const MIN_CALLS = 200;
|
|
12
18
|
/** Candidate thresholds as a share of the model's context window. */
|
|
@@ -33,6 +39,8 @@ export interface ModelRecords {
|
|
|
33
39
|
sessions: Turn[][];
|
|
34
40
|
compactions: Compaction[];
|
|
35
41
|
contextWindow: number;
|
|
42
|
+
/** Where the Host compacts when nothing is set (documented), or null when that is not known. */
|
|
43
|
+
defaultThreshold?: number | null;
|
|
36
44
|
}
|
|
37
45
|
export interface SummarizeOptions {
|
|
38
46
|
/** The Account's configured threshold in tokens; null is the Host default. */
|
|
@@ -77,6 +85,13 @@ export interface TuneEntry {
|
|
|
77
85
|
}
|
|
78
86
|
/** The Accounts that share one settings file: they get one suggestion and one thing to apply. */
|
|
79
87
|
export interface TuneGroup {
|
|
88
|
+
/** The size in the file is outside what Claude Code accepts, so it cannot be offered again. */
|
|
89
|
+
outOfRange: boolean;
|
|
90
|
+
/** Models that save an auto-compact size of their own, which wins over the file's size for them. */
|
|
91
|
+
overrides: {
|
|
92
|
+
model: string;
|
|
93
|
+
value: number;
|
|
94
|
+
}[];
|
|
80
95
|
/** The real settings file. */
|
|
81
96
|
file: string;
|
|
82
97
|
members: string[];
|
package/dist/src/core/tuning.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Relative cost of a token, input = 1. These are Anthropic's published multipliers, not prices (spec §3.18). */
|
|
2
2
|
export const WEIGHT = { input: 1, cacheRead: 0.1, write5m: 1.25, write1h: 2, output: 5 };
|
|
3
|
+
/**
|
|
4
|
+
* The sizes Claude Code accepts for the auto-compact window: 100K to 1M tokens, never above the model's context
|
|
5
|
+
* window (https://code.claude.com/docs/en/model-config#set-the-auto-compact-window).
|
|
6
|
+
*/
|
|
7
|
+
export const MIN_COMPACT_WINDOW = 100_000;
|
|
8
|
+
export const MAX_COMPACT_WINDOW = 1_000_000;
|
|
3
9
|
/** A model with fewer turns than this in the window gets no suggestion. */
|
|
4
10
|
export const MIN_CALLS = 200;
|
|
5
11
|
/** Candidate thresholds as a share of the model's context window. */
|
|
@@ -68,7 +74,13 @@ export function summarizeModel(rec, o) {
|
|
|
68
74
|
const pre = rec.compactions.map((c) => c.pre).filter((n) => n > 0);
|
|
69
75
|
const posts = rec.compactions.map((c) => c.post).filter((n) => n > 0);
|
|
70
76
|
const autoPre = auto.map((c) => c.pre).filter((n) => n > 0);
|
|
71
|
-
|
|
77
|
+
// What is in effect: the configured size, else what the Host did, else where it compacts by default.
|
|
78
|
+
// A size outside 100K-1M is not one Claude Code runs with (the documentation does not say what it does with it),
|
|
79
|
+
// so it is not taken as the baseline.
|
|
80
|
+
const usable = o.configured !== null && o.configured >= MIN_COMPACT_WINDOW && o.configured <= MAX_COMPACT_WINDOW;
|
|
81
|
+
const effective = usable && o.configured !== null
|
|
82
|
+
? Math.min(o.configured, rec.contextWindow)
|
|
83
|
+
: (median(autoPre) ?? rec.defaultThreshold ?? null);
|
|
72
84
|
const base = {
|
|
73
85
|
model: rec.model,
|
|
74
86
|
calls,
|
|
@@ -96,9 +108,16 @@ export function summarizeModel(rec, o) {
|
|
|
96
108
|
const post = median(posts);
|
|
97
109
|
if (post === null)
|
|
98
110
|
return stop('no-compactions');
|
|
111
|
+
// Only sizes Claude Code accepts: the lowest allowed one is always a candidate.
|
|
112
|
+
const top = Math.min(rec.contextWindow, MAX_COMPACT_WINDOW);
|
|
113
|
+
const candidates = new Set([MIN_COMPACT_WINDOW]);
|
|
114
|
+
for (const pct of CANDIDATE_PERCENTS)
|
|
115
|
+
candidates.add(roundTokens((rec.contextWindow * pct) / 100));
|
|
116
|
+
const sizes = [...candidates].filter((t) => t >= MIN_COMPACT_WINDOW && t <= top).sort((x, y) => x - y);
|
|
117
|
+
if (!sizes.length)
|
|
118
|
+
return stop('no-optimum');
|
|
99
119
|
let best = null;
|
|
100
|
-
for (const
|
|
101
|
-
const threshold = roundTokens((rec.contextWindow * pct) / 100);
|
|
120
|
+
for (const threshold of sizes) {
|
|
102
121
|
const cost = replay(rec.sessions, threshold, post);
|
|
103
122
|
// On a tie the larger threshold wins: fewer compactions for the same cost.
|
|
104
123
|
if (!best || cost < best.cost || (cost === best.cost && threshold > best.threshold))
|
|
@@ -106,10 +125,7 @@ export function summarizeModel(rec, o) {
|
|
|
106
125
|
}
|
|
107
126
|
if (!best)
|
|
108
127
|
return stop('insufficient-calls');
|
|
109
|
-
//
|
|
110
|
-
// is a cut-off of the search, not an optimum, and is not suggested.
|
|
111
|
-
if (best.threshold === roundTokens((rec.contextWindow * CANDIDATE_PERCENTS[0]) / 100))
|
|
112
|
-
return stop('no-optimum');
|
|
128
|
+
// The lowest allowed size can still be the cheapest: that is a real answer, "as low as Claude Code lets you".
|
|
113
129
|
if (effective === null)
|
|
114
130
|
return {
|
|
115
131
|
...base,
|
|
@@ -14,6 +14,12 @@ const SETTINGS_MAX_BYTES = 1_000_000;
|
|
|
14
14
|
/** Context window of a model, from the table or the records: a context that grew past 200K is a 1M window. */
|
|
15
15
|
export const isKnownClaudeModel = (id) => id.startsWith('claude-');
|
|
16
16
|
const windowOf = (maxCtx) => (maxCtx > 200_000 ? 1_000_000 : 200_000);
|
|
17
|
+
/**
|
|
18
|
+
* Models with a native 1M window, which compact at about 967K when nothing is set: the Fable models, Sonnet 5 and
|
|
19
|
+
* later, Haiku 5.5 and Opus 4.7 and later (https://code.claude.com/docs/en/model-config).
|
|
20
|
+
*/
|
|
21
|
+
const NATIVE_1M = /^claude-(fable|sonnet-[5-9]|haiku-[5-9]|opus-(4-[7-9]|[5-9]))/;
|
|
22
|
+
const NATIVE_DEFAULT_THRESHOLD = 967_000;
|
|
17
23
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0);
|
|
18
24
|
async function parseFile(file, since) {
|
|
19
25
|
const out = { lines: 0, parsed: 0, turns: [], compactions: [], failed: false };
|
|
@@ -180,8 +186,18 @@ export async function readClaudeTuning(account, ctx) {
|
|
|
180
186
|
const comps = resolved
|
|
181
187
|
.filter((c) => c[2] === model && c[0] >= since && c[0] <= now)
|
|
182
188
|
.map((c) => ({ ts: c[0], trigger: c[3], pre: c[4], post: c[5] }));
|
|
183
|
-
const
|
|
184
|
-
|
|
189
|
+
const native = NATIVE_1M.test(model);
|
|
190
|
+
// Elsewhere the window comes from the records: a context past 200K means the model runs with 1M.
|
|
191
|
+
const window = native
|
|
192
|
+
? 1_000_000
|
|
193
|
+
: Math.max(windowOf(maxCtx), windowOf(Math.max(0, ...comps.map((c) => c.pre))));
|
|
194
|
+
return {
|
|
195
|
+
model,
|
|
196
|
+
sessions: list,
|
|
197
|
+
compactions: comps,
|
|
198
|
+
contextWindow: window,
|
|
199
|
+
defaultThreshold: native ? NATIVE_DEFAULT_THRESHOLD : window,
|
|
200
|
+
};
|
|
185
201
|
});
|
|
186
202
|
return tuningResult(USAGE_DAYS, records, { configured, known: isKnownClaudeModel, mayRecommend: true });
|
|
187
203
|
}
|
package/dist/src/panel/i18n.d.ts
CHANGED
|
@@ -251,6 +251,8 @@ declare const en: {
|
|
|
251
251
|
'tune.noCompactions': string;
|
|
252
252
|
'tune.groupShared': string;
|
|
253
253
|
'tune.basis': string;
|
|
254
|
+
'tune.outOfRange': string;
|
|
255
|
+
'tune.overrides': string;
|
|
254
256
|
'tune.groupNone': string;
|
|
255
257
|
'tune.details': string;
|
|
256
258
|
'tune.detailsObserved': string;
|
package/dist/src/panel/i18n.js
CHANGED
|
@@ -253,6 +253,8 @@ const en = {
|
|
|
253
253
|
'tune.noCompactions': 'It did not compact in this period.',
|
|
254
254
|
'tune.groupShared': 'One setting for these accounts, because they share a settings file: {list}',
|
|
255
255
|
'tune.basis': 'Based on {model} on {ref}, the one used most.',
|
|
256
|
+
'tune.outOfRange': 'The size now in the file ({size}) is outside what Claude Code accepts (100K to 1M). Pick Default or a size from the menu to replace it.',
|
|
257
|
+
'tune.overrides': 'These models save a size of their own (set with /autocompact), which wins over this one: {list}',
|
|
256
258
|
'tune.groupNone': 'Not used enough to suggest a size yet. You can still pick one below.',
|
|
257
259
|
'tune.details': 'Details for each account',
|
|
258
260
|
'tune.detailsObserved': 'Only observed (there is no setting to change)',
|
|
@@ -277,7 +279,7 @@ const en = {
|
|
|
277
279
|
'tune.reason.already-best': 'The current setting is already about the cheapest we can estimate for how you work.',
|
|
278
280
|
'tune.reason.insufficient-calls': 'Not enough use in this period to estimate (needs {min} calls).',
|
|
279
281
|
'tune.reason.no-compactions': 'It has not compacted in this period, so there is nothing to estimate from.',
|
|
280
|
-
'tune.reason.no-optimum': '
|
|
282
|
+
'tune.reason.no-optimum': "This model's window is smaller than the lowest size Claude Code accepts (100K), so there is nothing to set.",
|
|
281
283
|
'tune.reason.unknown-model': 'This is not a Claude model, so sideby cannot estimate it.',
|
|
282
284
|
'tune.reason.observe-only': 'For Codex sideby only shows what happened; it cannot suggest a value yet.',
|
|
283
285
|
'tpage.title': 'Context tuning',
|
|
@@ -632,6 +634,8 @@ const zh = {
|
|
|
632
634
|
'tune.noCompactions': '这段时间没有压缩过。',
|
|
633
635
|
'tune.groupShared': '这些账号共用一个设置文件,所以用同一个设置:{list}',
|
|
634
636
|
'tune.basis': '按用得最多的 {model}({ref})来定。',
|
|
637
|
+
'tune.outOfRange': '文件里现在的大小({size})超出了 Claude Code 接受的范围(100K 到 1M)。请选「默认」或菜单里的某个大小来替换它。',
|
|
638
|
+
'tune.overrides': '这些模型自己保存了一个大小(用 /autocompact 设的),对它们来说那个优先于这里的设置:{list}',
|
|
635
639
|
'tune.groupNone': '用得还不够多,暂时给不出建议值。你仍然可以在下面自己选一个。',
|
|
636
640
|
'tune.details': '各账号的详情',
|
|
637
641
|
'tune.detailsObserved': '只做观测(没有可以改的设置)',
|
|
@@ -656,7 +660,7 @@ const zh = {
|
|
|
656
660
|
'tune.reason.already-best': '按你的用法,现在的设置已经差不多是我们能估算出的最省了。',
|
|
657
661
|
'tune.reason.insufficient-calls': '这段时间用得太少,估不准(至少要 {min} 次)。',
|
|
658
662
|
'tune.reason.no-compactions': '这段时间还没压缩过,没有可以参考的数据。',
|
|
659
|
-
'tune.reason.no-optimum': '
|
|
663
|
+
'tune.reason.no-optimum': '这个模型的窗口比 Claude Code 接受的最小值(100K)还小,没有可以设置的值。',
|
|
660
664
|
'tune.reason.unknown-model': '这不是 Claude 的模型,sideby 没法估算。',
|
|
661
665
|
'tune.reason.observe-only': 'Codex 目前只能看到发生了什么,还不能给建议值。',
|
|
662
666
|
'tpage.title': '上下文调优',
|
|
@@ -1315,6 +1315,7 @@ const MAIN = String.raw `
|
|
|
1315
1315
|
h('li', null, t('tune.change') + ': ' + tuneSize(plan.current) + ' → ' + tuneSize(plan.next)),
|
|
1316
1316
|
h('li', null, t('tune.file') + ': ', h('code', null, plan.file + (plan.creates ? ' ' + t('tune.willCreate') : '')))),
|
|
1317
1317
|
plan.affected.length > 1 ? h('p', { class: 'note note-warn' }, t('tune.shared', { list: plan.affected.join(', ') })) : null,
|
|
1318
|
+
plan.overrides && plan.overrides.length ? h('p', { class: 'note note-warn' }, t('tune.overrides', { list: plan.overrides.map((o) => o.model + ' (' + tokens(o.value) + ')').join(', ') })) : null,
|
|
1318
1319
|
h('p', { class: 'muted small' }, t('tune.newSessions')),
|
|
1319
1320
|
h('div', { class: 'row-actions' },
|
|
1320
1321
|
h('button', { class: 'btn btn-primary btn-sm', type: 'button', onclick: useConfirm }, t('tune.confirm')),
|
|
@@ -1323,12 +1324,12 @@ const MAIN = String.raw `
|
|
|
1323
1324
|
// The card for one settings file.
|
|
1324
1325
|
function tuneGroupCard(g, entries) {
|
|
1325
1326
|
const pick = g.pick
|
|
1326
|
-
const sizes = new Set([
|
|
1327
|
+
const sizes = new Set([100000, 150000, 200000, 300000, 500000, 800000, 1000000])
|
|
1327
1328
|
let top = 200000
|
|
1328
1329
|
for (const e of entries) if (e.tuning.status === 'ok') for (const m of e.tuning.models) top = Math.max(top, m.contextWindow)
|
|
1329
1330
|
for (const v of [...sizes]) if (v > top) sizes.delete(v)
|
|
1330
1331
|
if (pick) sizes.add(pick.threshold)
|
|
1331
|
-
if (g.current) sizes.add(g.current)
|
|
1332
|
+
if (g.current && !g.outOfRange) sizes.add(g.current)
|
|
1332
1333
|
const key = (v) => (v === null ? 'default' : String(v))
|
|
1333
1334
|
const chosen = tuneSel[g.id] !== undefined ? tuneSel[g.id] : key(g.current)
|
|
1334
1335
|
const sel = h('select', { 'aria-label': t('tune.setting') },
|
|
@@ -1346,7 +1347,13 @@ const MAIN = String.raw `
|
|
|
1346
1347
|
pick ? h('div', null, h('span', { class: 'muted small' }, t('tune.best')), h('div', { class: 'tune-num tune-best' }, tokens(pick.threshold))) : null))
|
|
1347
1348
|
if (pick && pick.savedPercent !== null) rows.push(h('p', { class: 'small' }, t('tune.saved', { n: Math.round(pick.savedPercent) })))
|
|
1348
1349
|
if (pick) rows.push(h('p', { class: 'muted small' }, t('tune.basis', { model: pick.model, ref: pick.ref })))
|
|
1349
|
-
else
|
|
1350
|
+
else {
|
|
1351
|
+
// Say why there is no suggestion: when a model is already at its cheapest, that is the news, not "not enough use".
|
|
1352
|
+
const why = entries.flatMap((e) => (e.tuning.status === 'ok' ? e.tuning.models : [])).some((m) => m.reason === 'already-best') ? t('tune.reason.already-best') : t('tune.groupNone')
|
|
1353
|
+
rows.push(h('p', { class: 'note' }, why))
|
|
1354
|
+
}
|
|
1355
|
+
if (g.overrides && g.overrides.length) rows.push(h('p', { class: 'note note-warn' }, t('tune.overrides', { list: g.overrides.map((o) => o.model + ' (' + tokens(o.value) + ')').join(', ') })))
|
|
1356
|
+
if (g.outOfRange) rows.push(h('p', { class: 'note note-warn' }, t('tune.outOfRange', { size: tokens(g.current) })))
|
|
1350
1357
|
if (!BOOT.readOnly) {
|
|
1351
1358
|
if (pick && !asking) rows.push(h('div', { class: 'row-actions' }, h('button', { class: 'btn btn-primary', type: 'button', onclick: () => useAsk(g, pick.threshold) }, t('tune.use', { size: tokens(pick.threshold) }))))
|
|
1352
1359
|
rows.push(h('div', { class: 'tune-setting' },
|
package/dist/src/runtime.js
CHANGED
|
@@ -15,8 +15,8 @@ import { resolvePaths } from './core/paths.js';
|
|
|
15
15
|
import { findResumeRoute } from './core/resume.js';
|
|
16
16
|
import { shareMeaning } from './core/share-meaning.js';
|
|
17
17
|
import { SHELLS, shellInitScript, writeShellInitFile, } from './core/shell-init.js';
|
|
18
|
-
import { groupSuggestion } from './core/tuning.js';
|
|
19
|
-
import { applyTune,
|
|
18
|
+
import { groupSuggestion, MAX_COMPACT_WINDOW, MIN_COMPACT_WINDOW, } from './core/tuning.js';
|
|
19
|
+
import { applyTune, planTuneApply, readTuneSettings, settingsFileOf, tuneUndoInfo, undoTune, } from './core/tuning-apply.js';
|
|
20
20
|
import { packageVersion } from './core/version.js';
|
|
21
21
|
import { BUILTIN_PLUGINS } from './families/index.js';
|
|
22
22
|
import { HookBus } from './plugins/bus.js';
|
|
@@ -492,7 +492,7 @@ export async function createRuntime(opts = {}) {
|
|
|
492
492
|
for (const m of members)
|
|
493
493
|
list.push(await entryOf(m));
|
|
494
494
|
const pick = groupSuggestion(list);
|
|
495
|
-
const current = await
|
|
495
|
+
const { current, overrides } = await readTuneSettings(members[0]);
|
|
496
496
|
const target = members.find((m) => m.ref === pick?.ref) ?? members[0];
|
|
497
497
|
const undo = await tuneUndoInfo(target, readContext());
|
|
498
498
|
groups.push({
|
|
@@ -500,6 +500,8 @@ export async function createRuntime(opts = {}) {
|
|
|
500
500
|
members: members.map((m) => m.ref),
|
|
501
501
|
target: target.ref,
|
|
502
502
|
current,
|
|
503
|
+
outOfRange: current !== null && (current < MIN_COMPACT_WINDOW || current > MAX_COMPACT_WINDOW),
|
|
504
|
+
overrides,
|
|
503
505
|
pick,
|
|
504
506
|
undo: undo ? { value: undo.applied, previous: undo.previous } : null,
|
|
505
507
|
});
|
package/package.json
CHANGED
package/schemas/tune-change.json
CHANGED
|
@@ -95,6 +95,24 @@
|
|
|
95
95
|
"creates": {
|
|
96
96
|
"type": "boolean"
|
|
97
97
|
},
|
|
98
|
+
"overrides": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"required": [
|
|
103
|
+
"model",
|
|
104
|
+
"value"
|
|
105
|
+
],
|
|
106
|
+
"properties": {
|
|
107
|
+
"model": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
},
|
|
110
|
+
"value": {
|
|
111
|
+
"type": "number"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
98
116
|
"reason": {
|
|
99
117
|
"type": "string"
|
|
100
118
|
}
|
package/skills/sideby/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: sideby
|
|
3
3
|
description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.4.
|
|
5
|
+
version: "0.4.1"
|
|
6
6
|
requires:
|
|
7
7
|
bins: [sideby, node]
|
|
8
8
|
cliHelp: sideby --help
|
|
@@ -48,4 +48,4 @@ Quota is only as fresh as each account's last session; `observedAt` says when it
|
|
|
48
48
|
|
|
49
49
|
## Context tuning
|
|
50
50
|
|
|
51
|
-
`sideby tune [ref|family] --json` estimates, per model, the auto-compact threshold that spends the fewest tokens, from the account's last 7 days of local sessions. It is read-only and an estimate: each model has `reason` (`suggested` carries `suggestion.threshold`; the others say why there is none: `insufficient-calls`, `no-compactions`, `no-optimum` (the
|
|
51
|
+
`sideby tune [ref|family] --json` estimates, per model, the auto-compact threshold that spends the fewest tokens, from the account's last 7 days of local sessions. It is read-only and an estimate: each model has `reason` (`suggested` carries `suggestion.threshold`; the others say why there is none: `insufficient-calls`, `no-compactions`, `no-optimum` (the model window is below 100K, the lowest size Claude Code accepts), `unknown-model`, `already-best`, `observe-only`). Only Claude gets suggestions; Codex is observation only; Grok and pi return `no-source`. The analysis changes nothing. To use a suggestion run `sideby tune apply <ref> [tokens]`: it prints the file, the change and every account sharing that file (a linked `settings.json` is one shared file) and exits 10. Show that to the user in plain words, and only after a yes rerun with `--yes`. `sideby tune undo <ref> --yes` puts the old value back. Do not hand-edit `settings.json` for the user.
|