sideby 0.3.0 → 0.4.0
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/README.md +27 -10
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +8 -1
- package/dist/src/cli/commands.js +330 -9
- package/dist/src/cli/format.d.ts +3 -1
- package/dist/src/cli/format.js +25 -0
- package/dist/src/cli/json-schemas.js +113 -0
- package/dist/src/cli/main.d.ts +5 -0
- package/dist/src/cli/main.js +40 -8
- package/dist/src/core/config.d.ts +40 -0
- package/dist/src/core/config.js +83 -11
- package/dist/src/core/file-memo.d.ts +23 -0
- package/dist/src/core/file-memo.js +37 -0
- package/dist/src/core/handoff-brief.d.ts +17 -0
- package/dist/src/core/handoff-brief.js +75 -0
- package/dist/src/core/handoff-chain.d.ts +74 -0
- package/dist/src/core/handoff-chain.js +74 -0
- package/dist/src/core/handoff-doctor.d.ts +13 -0
- package/dist/src/core/handoff-doctor.js +35 -0
- package/dist/src/core/handoff-run.d.ts +31 -0
- package/dist/src/core/handoff-run.js +346 -0
- package/dist/src/core/handoff-select.d.ts +42 -0
- package/dist/src/core/handoff-select.js +88 -0
- package/dist/src/core/handoff-status.d.ts +66 -0
- package/dist/src/core/handoff-status.js +198 -0
- package/dist/src/core/launch.d.ts +15 -0
- package/dist/src/core/launch.js +25 -2
- package/dist/src/core/memo-store.d.ts +20 -0
- package/dist/src/core/memo-store.js +92 -0
- package/dist/src/core/resume.d.ts +7 -2
- package/dist/src/core/resume.js +22 -9
- package/dist/src/core/tuning-apply.d.ts +51 -0
- package/dist/src/core/tuning-apply.js +237 -0
- package/dist/src/core/tuning.d.ts +98 -0
- package/dist/src/core/tuning.js +176 -0
- package/dist/src/families/claude/brief.d.ts +5 -0
- package/dist/src/families/claude/brief.js +63 -0
- package/dist/src/families/claude/handoff-hook.d.ts +2 -0
- package/dist/src/families/claude/handoff-hook.js +31 -0
- package/dist/src/families/claude/handoff.d.ts +5 -0
- package/dist/src/families/claude/handoff.js +115 -0
- package/dist/src/families/claude/index.js +8 -1
- package/dist/src/families/claude/tuning.d.ts +22 -0
- package/dist/src/families/claude/tuning.js +187 -0
- package/dist/src/families/claude/usage.d.ts +1 -0
- package/dist/src/families/claude/usage.js +2 -2
- package/dist/src/families/codex/brief.d.ts +5 -0
- package/dist/src/families/codex/brief.js +49 -0
- package/dist/src/families/codex/handoff-hook.d.ts +2 -0
- package/dist/src/families/codex/handoff-hook.js +16 -0
- package/dist/src/families/codex/handoff.d.ts +5 -0
- package/dist/src/families/codex/handoff.js +121 -0
- package/dist/src/families/codex/index.js +4 -0
- package/dist/src/families/codex/rate-limits.d.ts +16 -0
- package/dist/src/families/codex/rate-limits.js +92 -0
- package/dist/src/families/codex/rollout.d.ts +1 -2
- package/dist/src/families/codex/rollout.js +4 -48
- package/dist/src/families/codex/tuning.d.ts +16 -0
- package/dist/src/families/codex/tuning.js +145 -0
- package/dist/src/families/grok/brief.d.ts +5 -0
- package/dist/src/families/grok/brief.js +74 -0
- package/dist/src/families/grok/handoff-hook.d.ts +4 -0
- package/dist/src/families/grok/handoff-hook.js +26 -0
- package/dist/src/families/grok/handoff.d.ts +47 -0
- package/dist/src/families/grok/handoff.js +219 -0
- package/dist/src/families/grok/index.js +2 -0
- package/dist/src/families/pi/handoff.d.ts +2 -0
- package/dist/src/families/pi/handoff.js +13 -0
- package/dist/src/families/pi/index.js +2 -0
- package/dist/src/families/shared/brief.d.ts +24 -0
- package/dist/src/families/shared/brief.js +42 -0
- package/dist/src/families/shared/handoff-args.d.ts +18 -0
- package/dist/src/families/shared/handoff-args.js +57 -0
- package/dist/src/families/shared/sessions.d.ts +4 -0
- package/dist/src/families/shared/sessions.js +19 -0
- package/dist/src/families/shared/toml.d.ts +5 -0
- package/dist/src/families/shared/toml.js +30 -0
- package/dist/src/handoff/hook.d.ts +16 -0
- package/dist/src/handoff/hook.js +166 -0
- package/dist/src/handoff/messages.d.ts +7 -0
- package/dist/src/handoff/messages.js +27 -0
- package/dist/src/handoff/reader.d.ts +11 -0
- package/dist/src/handoff/reader.js +9 -0
- package/dist/src/panel/handler.d.ts +6 -0
- package/dist/src/panel/handler.js +110 -0
- package/dist/src/panel/i18n.d.ts +96 -0
- package/dist/src/panel/i18n.js +192 -0
- package/dist/src/panel/page-logic.d.ts +49 -0
- package/dist/src/panel/page-logic.js +67 -0
- package/dist/src/panel/page-script.js +454 -4
- package/dist/src/panel/page-styles.js +28 -2
- package/dist/src/panel/page.js +4 -1
- package/dist/src/quota/claude-statusline.js +4 -7
- package/dist/src/runtime.d.ts +40 -3
- package/dist/src/runtime.js +210 -41
- package/dist/src/types.d.ts +93 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +1 -0
- package/package.json +16 -4
- package/schemas/config.json +73 -0
- package/schemas/families.json +5 -1
- package/schemas/handoff-disable.json +106 -0
- package/schemas/handoff-enable.json +106 -0
- package/schemas/handoff-ready.json +23 -0
- package/schemas/handoff-setup.json +60 -0
- package/schemas/handoff-status.json +137 -0
- package/schemas/handoff-teardown.json +30 -0
- package/schemas/list.json +5 -1
- package/schemas/tune-change.json +108 -0
- package/schemas/tune.json +243 -0
- package/skills/sideby/SKILL.md +5 -3
- package/skills/sideby/references/badge.md +1 -1
- package/skills/sideby/references/diagnose.md +4 -0
- package/skills/sideby/references/handoff.md +24 -2
- package/README.zh-CN.md +0 -112
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Codex `rate_limits` in rollout `token_count` events: the windows the Quota reader and the handoff hook share.
|
|
2
|
+
// The hook runs on every tool call, so this module imports only Node built-ins (spec §3.17).
|
|
3
|
+
import { open } from 'node:fs/promises';
|
|
4
|
+
export const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
5
|
+
export const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
6
|
+
const MAX_RESETS_AT_SECONDS = 1e11;
|
|
7
|
+
export function windowLabel(minutes) {
|
|
8
|
+
if (minutes === 300)
|
|
9
|
+
return '5h';
|
|
10
|
+
if (minutes === 10080)
|
|
11
|
+
return '7d';
|
|
12
|
+
return `${minutes}m`;
|
|
13
|
+
}
|
|
14
|
+
function quotaWindow(raw) {
|
|
15
|
+
if (!isObj(raw))
|
|
16
|
+
return null;
|
|
17
|
+
const { used_percent: used, window_minutes: minutes, resets_at: resets } = raw;
|
|
18
|
+
if (!isNum(used) || !isNum(minutes) || minutes <= 0)
|
|
19
|
+
return null;
|
|
20
|
+
// Seconds since the epoch; a millisecond value would land tens of thousands of years ahead.
|
|
21
|
+
if (!isNum(resets) || resets <= 0 || resets >= MAX_RESETS_AT_SECONDS)
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
label: windowLabel(minutes),
|
|
25
|
+
windowMinutes: minutes,
|
|
26
|
+
usedPercent: used,
|
|
27
|
+
resetsAt: new Date(resets * 1000).toISOString(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** The windows of one `token_count` payload, or null when its `rate_limits` is missing or malformed. */
|
|
31
|
+
export function rateLimits(payload) {
|
|
32
|
+
const rl = payload.rate_limits;
|
|
33
|
+
if (!isObj(rl))
|
|
34
|
+
return null;
|
|
35
|
+
const primary = quotaWindow(rl.primary);
|
|
36
|
+
if (!primary)
|
|
37
|
+
return null;
|
|
38
|
+
const windows = [primary];
|
|
39
|
+
if (rl.secondary !== null && rl.secondary !== undefined) {
|
|
40
|
+
const secondary = quotaWindow(rl.secondary);
|
|
41
|
+
if (!secondary)
|
|
42
|
+
return null;
|
|
43
|
+
windows.push(secondary);
|
|
44
|
+
}
|
|
45
|
+
return { windows, ...(typeof rl.plan_type === 'string' ? { plan: rl.plan_type } : {}) };
|
|
46
|
+
}
|
|
47
|
+
/** How much of a rollout's end the hook reads: `token_count` events come at least once per turn. */
|
|
48
|
+
const TAIL_BYTES = 256 * 1024;
|
|
49
|
+
/**
|
|
50
|
+
* The windows of the last well-formed `rate_limits` in the rollout's last 256 KB, or null. A line cut by the read
|
|
51
|
+
* window or still being written is skipped. Codex writes `rate_limits` after caching them, so they may lag.
|
|
52
|
+
*/
|
|
53
|
+
export async function lastRateLimits(file) {
|
|
54
|
+
let text;
|
|
55
|
+
try {
|
|
56
|
+
const fh = await open(file, 'r');
|
|
57
|
+
try {
|
|
58
|
+
const { size } = await fh.stat();
|
|
59
|
+
const start = Math.max(0, size - TAIL_BYTES);
|
|
60
|
+
const buf = Buffer.alloc(size - start);
|
|
61
|
+
await fh.read(buf, 0, buf.length, start);
|
|
62
|
+
text = buf.toString('utf8');
|
|
63
|
+
if (start > 0)
|
|
64
|
+
text = text.slice(text.indexOf('\n') + 1);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await fh.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const lines = text.split('\n');
|
|
74
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
75
|
+
const line = lines[i];
|
|
76
|
+
if (!line.includes('rate_limits'))
|
|
77
|
+
continue;
|
|
78
|
+
let o;
|
|
79
|
+
try {
|
|
80
|
+
o = JSON.parse(line);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!isObj(o) || !isObj(o.payload) || o.payload.type !== 'token_count')
|
|
86
|
+
continue;
|
|
87
|
+
const rl = rateLimits(o.payload);
|
|
88
|
+
if (rl)
|
|
89
|
+
return rl.windows;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FileMemo, type FileStats } from '../../core/file-memo.ts';
|
|
2
2
|
import type { QuotaResult, QuotaWindow, UsageResult } from '../../types.ts';
|
|
3
|
+
export { windowLabel } from './rate-limits.ts';
|
|
3
4
|
export declare const MAX_QUOTA_FILES = 20;
|
|
4
5
|
export interface RolloutFile {
|
|
5
6
|
path: string;
|
|
@@ -8,7 +9,6 @@ export interface RolloutFile {
|
|
|
8
9
|
}
|
|
9
10
|
/** Every rollout file under `<dir>/sessions`, newest mtime first. */
|
|
10
11
|
export declare function listRollouts(dir: string): Promise<RolloutFile[]>;
|
|
11
|
-
export declare function windowLabel(minutes: number): string;
|
|
12
12
|
interface FileLimits {
|
|
13
13
|
/** The file's newest well-formed `rate_limits` (the later line on a tie), or null. */
|
|
14
14
|
best: {
|
|
@@ -64,4 +64,3 @@ export declare const codexUsageMemo: FileMemo<FileUsage>;
|
|
|
64
64
|
* adds only its recent turns. `daily` spreads the same increments over local days.
|
|
65
65
|
*/
|
|
66
66
|
export declare function readCodexUsage(dir: string, now: Date, days?: number): Promise<UsageResult>;
|
|
67
|
-
export {};
|
|
@@ -8,15 +8,10 @@ import { createInterface } from 'node:readline';
|
|
|
8
8
|
import { FileMemo } from '../../core/file-memo.js';
|
|
9
9
|
import { USAGE_DAYS } from '../../core/quota-levels.js';
|
|
10
10
|
import { dailyBuckets } from '../../core/usage-days.js';
|
|
11
|
+
import { isNum, isObj, rateLimits } from './rate-limits.js';
|
|
12
|
+
export { windowLabel } from './rate-limits.js';
|
|
11
13
|
export const MAX_QUOTA_FILES = 20;
|
|
12
14
|
const ROLLOUT = /^rollout-.*\.jsonl$/;
|
|
13
|
-
const MAX_RESETS_AT_SECONDS = 1e11;
|
|
14
|
-
function isObj(v) {
|
|
15
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
16
|
-
}
|
|
17
|
-
function isNum(v) {
|
|
18
|
-
return typeof v === 'number' && Number.isFinite(v);
|
|
19
|
-
}
|
|
20
15
|
/** Every rollout file under `<dir>/sessions`, newest mtime first. */
|
|
21
16
|
export async function listRollouts(dir) {
|
|
22
17
|
const root = join(dir, 'sessions');
|
|
@@ -75,45 +70,6 @@ async function* tokenCounts(file, read = { failed: false }) {
|
|
|
75
70
|
lines.close();
|
|
76
71
|
}
|
|
77
72
|
}
|
|
78
|
-
export function windowLabel(minutes) {
|
|
79
|
-
if (minutes === 300)
|
|
80
|
-
return '5h';
|
|
81
|
-
if (minutes === 10080)
|
|
82
|
-
return '7d';
|
|
83
|
-
return `${minutes}m`;
|
|
84
|
-
}
|
|
85
|
-
function quotaWindow(raw) {
|
|
86
|
-
if (!isObj(raw))
|
|
87
|
-
return null;
|
|
88
|
-
const { used_percent: used, window_minutes: minutes, resets_at: resets } = raw;
|
|
89
|
-
if (!isNum(used) || !isNum(minutes) || minutes <= 0)
|
|
90
|
-
return null;
|
|
91
|
-
// Seconds since the epoch; a millisecond value would land tens of thousands of years ahead.
|
|
92
|
-
if (!isNum(resets) || resets <= 0 || resets >= MAX_RESETS_AT_SECONDS)
|
|
93
|
-
return null;
|
|
94
|
-
return {
|
|
95
|
-
label: windowLabel(minutes),
|
|
96
|
-
windowMinutes: minutes,
|
|
97
|
-
usedPercent: used,
|
|
98
|
-
resetsAt: new Date(resets * 1000).toISOString(),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function rateLimits(payload) {
|
|
102
|
-
const rl = payload.rate_limits;
|
|
103
|
-
if (!isObj(rl))
|
|
104
|
-
return null;
|
|
105
|
-
const primary = quotaWindow(rl.primary);
|
|
106
|
-
if (!primary)
|
|
107
|
-
return null;
|
|
108
|
-
const windows = [primary];
|
|
109
|
-
if (rl.secondary !== null && rl.secondary !== undefined) {
|
|
110
|
-
const secondary = quotaWindow(rl.secondary);
|
|
111
|
-
if (!secondary)
|
|
112
|
-
return null;
|
|
113
|
-
windows.push(secondary);
|
|
114
|
-
}
|
|
115
|
-
return { windows, ...(typeof rl.plan_type === 'string' ? { plan: rl.plan_type } : {}) };
|
|
116
|
-
}
|
|
117
73
|
async function fileLimits(file) {
|
|
118
74
|
const read = { failed: false };
|
|
119
75
|
let best = null;
|
|
@@ -127,7 +83,7 @@ async function fileLimits(file) {
|
|
|
127
83
|
return { best, failed: read.failed };
|
|
128
84
|
}
|
|
129
85
|
/** Parsed `rate_limits` per Account directory; a rollout is read again only after it changed. */
|
|
130
|
-
export const codexQuotaMemo = new FileMemo();
|
|
86
|
+
export const codexQuotaMemo = new FileMemo({ name: 'codex-quota', version: 1 });
|
|
131
87
|
/** Newest well-formed `rate_limits` across the newest rollout files, by event timestamp. */
|
|
132
88
|
export async function readCodexQuota(dir) {
|
|
133
89
|
const files = (await listRollouts(dir)).slice(0, MAX_QUOTA_FILES);
|
|
@@ -231,7 +187,7 @@ async function fileUsage(file) {
|
|
|
231
187
|
return { times, events, deltas: fileDeltas(events), failed: read.failed };
|
|
232
188
|
}
|
|
233
189
|
/** Parsed token totals per Account directory; a rollout is read again only after it changed. */
|
|
234
|
-
export const codexUsageMemo = new FileMemo();
|
|
190
|
+
export const codexUsageMemo = new FileMemo({ name: 'codex-usage', version: 1 });
|
|
235
191
|
/**
|
|
236
192
|
* Token totals for events in the last `days` x 24 h, from rollout files modified in that window (mtime only
|
|
237
193
|
* picks which files to read). Each event counts its fileDeltas increment, so a long session resumed today
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FileMemo } from '../../core/file-memo.ts';
|
|
2
|
+
import type { Account, ReadContext, TuningResult } from '../../types.ts';
|
|
3
|
+
/** What one rollout contributes: per `token_count` the context size, and each compaction's before and after. */
|
|
4
|
+
interface Summary {
|
|
5
|
+
/** `[ts, model, ctx, window]` */
|
|
6
|
+
turns: [number, string, number, number][];
|
|
7
|
+
/** `[ts, model, pre, post]` */
|
|
8
|
+
compactions: [number, string, number, number][];
|
|
9
|
+
failed: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Per-rollout summaries per Account directory; a rollout is parsed again only after it changed. */
|
|
12
|
+
export declare const codexTuningMemo: FileMemo<Summary>;
|
|
13
|
+
/** The Account's configured threshold, or null for the Host default. Takes one top-level key; nothing else is kept. */
|
|
14
|
+
export declare function readCompactLimit(dir: string): Promise<number | null>;
|
|
15
|
+
export declare function readCodexTuning(account: Account, ctx: ReadContext): Promise<TuningResult>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Context tuning for a Codex Account (spec §3.18): observation only. The default Compaction Threshold and its
|
|
2
|
+
// scope have no first-hand source, so this shows how large the context was when Codex compacted, never a
|
|
3
|
+
// suggestion. The one Host config value read is `model_auto_compact_token_limit` from the Account's `config.toml`.
|
|
4
|
+
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
import { FileMemo } from '../../core/file-memo.js';
|
|
9
|
+
import { USAGE_DAYS } from '../../core/quota-levels.js';
|
|
10
|
+
import { getOrCreate, tuningResult, } from '../../core/tuning.js';
|
|
11
|
+
import { listRollouts } from './rollout.js';
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
const CONFIG_MAX_BYTES = 1_000_000;
|
|
14
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0);
|
|
16
|
+
async function parseFile(file, since) {
|
|
17
|
+
const out = { turns: [], compactions: [], failed: false };
|
|
18
|
+
let model = '';
|
|
19
|
+
let last = 0;
|
|
20
|
+
let pending = null;
|
|
21
|
+
try {
|
|
22
|
+
const rl = createInterface({ input: createReadStream(file), crlfDelay: Number.POSITIVE_INFINITY });
|
|
23
|
+
for await (const line of rl) {
|
|
24
|
+
if (!line.includes('token_count') && !line.includes('"compacted"') && !line.includes('turn_context'))
|
|
25
|
+
continue;
|
|
26
|
+
let row;
|
|
27
|
+
try {
|
|
28
|
+
row = JSON.parse(line);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!isObj(row))
|
|
34
|
+
continue;
|
|
35
|
+
const payload = isObj(row.payload) ? row.payload : {};
|
|
36
|
+
const ts = typeof row.timestamp === 'string' ? Date.parse(row.timestamp) : Number.NaN;
|
|
37
|
+
if (row.type === 'turn_context') {
|
|
38
|
+
if (typeof payload.model === 'string' && payload.model)
|
|
39
|
+
model = payload.model;
|
|
40
|
+
}
|
|
41
|
+
else if (row.type === 'compacted') {
|
|
42
|
+
if (Number.isFinite(ts) && last > 0)
|
|
43
|
+
pending = { ts, model, pre: last };
|
|
44
|
+
}
|
|
45
|
+
else if (row.type === 'event_msg' && payload.type === 'token_count') {
|
|
46
|
+
const info = isObj(payload.info) ? payload.info : null;
|
|
47
|
+
const usage = info && isObj(info.last_token_usage) ? info.last_token_usage : null;
|
|
48
|
+
const ctx = usage ? num(usage.input_tokens) : 0;
|
|
49
|
+
if (!ctx || !Number.isFinite(ts))
|
|
50
|
+
continue;
|
|
51
|
+
last = ctx;
|
|
52
|
+
if (pending) {
|
|
53
|
+
if (pending.ts >= since && pending.model)
|
|
54
|
+
out.compactions.push([pending.ts, pending.model, pending.pre, ctx]);
|
|
55
|
+
pending = null;
|
|
56
|
+
}
|
|
57
|
+
if (ts >= since && model)
|
|
58
|
+
out.turns.push([ts, model, ctx, num(info?.model_context_window)]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
out.failed = true;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** Per-rollout summaries per Account directory; a rollout is parsed again only after it changed. */
|
|
68
|
+
export const codexTuningMemo = new FileMemo({ name: 'codex-tuning', version: 1 });
|
|
69
|
+
/** The Account's configured threshold, or null for the Host default. Takes one top-level key; nothing else is kept. */
|
|
70
|
+
export async function readCompactLimit(dir) {
|
|
71
|
+
try {
|
|
72
|
+
const file = join(dir, 'config.toml');
|
|
73
|
+
const st = await stat(file);
|
|
74
|
+
if (!st.isFile() || st.size > CONFIG_MAX_BYTES)
|
|
75
|
+
return null;
|
|
76
|
+
for (const raw of (await readFile(file, 'utf8')).split('\n')) {
|
|
77
|
+
const line = raw.trim();
|
|
78
|
+
if (line.startsWith('['))
|
|
79
|
+
return null; // a table starts: no top-level key is left
|
|
80
|
+
const m = /^model_auto_compact_token_limit\s*=\s*([0-9_]+)\s*(#.*)?$/.exec(line);
|
|
81
|
+
if (m) {
|
|
82
|
+
const n = Number((m[1] ?? '').replaceAll('_', ''));
|
|
83
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// unreadable config: the Host default
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
export async function readCodexTuning(account, ctx) {
|
|
93
|
+
const now = ctx.now.getTime();
|
|
94
|
+
const since = now - USAGE_DAYS * DAY_MS;
|
|
95
|
+
const files = (await listRollouts(account.dir)).filter((f) => f.mtimeMs >= since);
|
|
96
|
+
if (!files.length)
|
|
97
|
+
return {
|
|
98
|
+
status: 'unavailable',
|
|
99
|
+
reason: 'no-session',
|
|
100
|
+
detail: `no Codex session in the last ${USAGE_DAYS} days`,
|
|
101
|
+
};
|
|
102
|
+
const memo = codexTuningMemo.pass(account.dir);
|
|
103
|
+
const byModel = new Map();
|
|
104
|
+
const entry = (model) => getOrCreate(byModel, model, () => ({
|
|
105
|
+
sessions: [],
|
|
106
|
+
compactions: [],
|
|
107
|
+
window: 0,
|
|
108
|
+
maxCtx: 0,
|
|
109
|
+
}));
|
|
110
|
+
for (const f of files) {
|
|
111
|
+
const s = await memo.get(f.path, f.stats, () => parseFile(f.path, since), (v) => !v.failed);
|
|
112
|
+
const perModel = new Map();
|
|
113
|
+
for (const [ts, model, c, window] of s.turns) {
|
|
114
|
+
if (ts > now || ts < since)
|
|
115
|
+
continue;
|
|
116
|
+
const e = entry(model);
|
|
117
|
+
e.window = Math.max(e.window, window);
|
|
118
|
+
e.maxCtx = Math.max(e.maxCtx, c);
|
|
119
|
+
const list = getOrCreate(perModel, model, () => []);
|
|
120
|
+
list.push({ ts, ctx: c, out: 0, write5m: 0, write1h: 0 });
|
|
121
|
+
}
|
|
122
|
+
for (const [model, turns] of perModel)
|
|
123
|
+
entry(model).sessions.push(turns);
|
|
124
|
+
for (const [ts, model, pre, post] of s.compactions) {
|
|
125
|
+
if (ts > now || ts < since)
|
|
126
|
+
continue;
|
|
127
|
+
entry(model).compactions.push({ ts, trigger: 'unknown', pre, post });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
memo.done();
|
|
131
|
+
if (!byModel.size)
|
|
132
|
+
return {
|
|
133
|
+
status: 'unavailable',
|
|
134
|
+
reason: 'no-session',
|
|
135
|
+
detail: `no Codex token counts in the last ${USAGE_DAYS} days`,
|
|
136
|
+
};
|
|
137
|
+
const configured = await readCompactLimit(account.dir);
|
|
138
|
+
const records = [...byModel].map(([model, e]) => ({
|
|
139
|
+
model,
|
|
140
|
+
sessions: e.sessions,
|
|
141
|
+
compactions: e.compactions,
|
|
142
|
+
contextWindow: e.window || e.maxCtx,
|
|
143
|
+
}));
|
|
144
|
+
return tuningResult(USAGE_DAYS, records, { configured, known: () => false, mayRecommend: false });
|
|
145
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// A Brief from a Grok Build session (`sessions/<encoded cwd>/<id>/chat_history.jsonl`), for when the session hit its
|
|
2
|
+
// limit before writing one. Grok's hook input carries the session id but no transcript path.
|
|
3
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { DigestBuilder, formatDigest } from '../shared/brief.js';
|
|
6
|
+
const EDIT_TOOL = /edit|write|replace|patch/i;
|
|
7
|
+
async function findSession(dir, id) {
|
|
8
|
+
try {
|
|
9
|
+
for (const d of await readdir(join(dir, 'sessions'), { withFileTypes: true })) {
|
|
10
|
+
if (!d.isDirectory())
|
|
11
|
+
continue;
|
|
12
|
+
const p = join(dir, 'sessions', d.name, id, 'chat_history.jsonl');
|
|
13
|
+
try {
|
|
14
|
+
await stat(p);
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// not under this directory
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// no sessions yet
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
export async function grokBrief(account, session) {
|
|
28
|
+
const file = session.transcriptPath ?? (session.id ? await findSession(account.dir, session.id) : undefined);
|
|
29
|
+
if (!file)
|
|
30
|
+
return undefined;
|
|
31
|
+
let text;
|
|
32
|
+
try {
|
|
33
|
+
text = await readFile(file, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const b = new DigestBuilder();
|
|
39
|
+
for (const line of text.split('\n')) {
|
|
40
|
+
let o;
|
|
41
|
+
try {
|
|
42
|
+
o = JSON.parse(line);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (o.type === 'user' && Array.isArray(o.content)) {
|
|
48
|
+
for (const c of o.content)
|
|
49
|
+
if (c?.type === 'text' && typeof c.text === 'string')
|
|
50
|
+
b.user(c.text);
|
|
51
|
+
}
|
|
52
|
+
else if (o.type === 'assistant') {
|
|
53
|
+
if (typeof o.content === 'string')
|
|
54
|
+
b.assistant(o.content);
|
|
55
|
+
for (const call of Array.isArray(o.tool_calls)
|
|
56
|
+
? o.tool_calls
|
|
57
|
+
: []) {
|
|
58
|
+
if (typeof call?.name !== 'string' ||
|
|
59
|
+
!EDIT_TOOL.test(call.name) ||
|
|
60
|
+
typeof call.arguments !== 'string')
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const args = JSON.parse(call.arguments);
|
|
64
|
+
b.file(args.path ?? args.file_path ?? args.target_file);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// arguments not JSON
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const d = b.result();
|
|
73
|
+
return d ? formatDigest(d) : undefined;
|
|
74
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type HookReader } from '../../handoff/reader.ts';
|
|
2
|
+
/** Substrings that mean the usage pool or credits are used up. `StopFailure.error` has no class of its own for it. */
|
|
3
|
+
export declare const GROK_USED_UP: string[];
|
|
4
|
+
export declare const grokHookReader: HookReader;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Grok Build for the handoff hook (spec §3.17): no public Quota, so only a turn that failed at the limit starts a
|
|
2
|
+
// Handoff. Leaf module. The wording comes from the grok 1.0.50 binary; check it again when Grok changes.
|
|
3
|
+
import { field } from '../../handoff/reader.js';
|
|
4
|
+
/** Substrings that mean the usage pool or credits are used up. `StopFailure.error` has no class of its own for it. */
|
|
5
|
+
export const GROK_USED_UP = [
|
|
6
|
+
'You hit your weekly limit',
|
|
7
|
+
'You hit your free usage limit',
|
|
8
|
+
'reached your free Grok Build usage limit',
|
|
9
|
+
'usage limit reached',
|
|
10
|
+
'out of credits',
|
|
11
|
+
'usage balance exhausted',
|
|
12
|
+
'spending cap',
|
|
13
|
+
'credit limit for your plan',
|
|
14
|
+
'free-usage-exhausted',
|
|
15
|
+
'status 402',
|
|
16
|
+
];
|
|
17
|
+
export const grokHookReader = {
|
|
18
|
+
limitHit(input) {
|
|
19
|
+
if (field(input, 'subagentType'))
|
|
20
|
+
return false;
|
|
21
|
+
// `status 403` is not listed: alone it may be a refused request, and next to a used-up message that message
|
|
22
|
+
// matches by itself. A plain "rate limit … try again later" is transient and matches nothing here.
|
|
23
|
+
const text = `${field(input, 'errorDetails') ?? ''}\n${field(input, 'lastAssistantMessage') ?? ''}`;
|
|
24
|
+
return GROK_USED_UP.some((s) => text.includes(s));
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Env, FamilyHandoff, QuotaSetup } from '../../types.ts';
|
|
2
|
+
import { type ArgRules } from '../shared/handoff-args.ts';
|
|
3
|
+
/** From `grok --help`. */
|
|
4
|
+
export declare const GROK_ARGS: ArgRules;
|
|
5
|
+
export declare const ETC_GROK = "/etc/grok";
|
|
6
|
+
export declare const HOOK_FILE = "sideby-handoff.json";
|
|
7
|
+
/** The text of Grok's config layers that decide the sandbox, leader and hook policy; missing files are null. */
|
|
8
|
+
export interface GrokLayers {
|
|
9
|
+
etcRequirements: string | null;
|
|
10
|
+
homeRequirements: string | null;
|
|
11
|
+
config: string | null;
|
|
12
|
+
homeManaged: string | null;
|
|
13
|
+
etcManaged: string | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The effective sandbox profile, leader setting and managed-hooks pin, in Grok's precedence (spec §3.17):
|
|
17
|
+
* requirements (`/etc` over `$GROK_HOME`, a pin) > command line > environment > `config.toml` > managed config
|
|
18
|
+
* (`$GROK_HOME` over `/etc`) > the default `off`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function grokPolicy(layers: GrokLayers, args: readonly string[], env: Env): {
|
|
21
|
+
profile: string;
|
|
22
|
+
leader: boolean;
|
|
23
|
+
managedHooksOnly: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function grokEligibility(policy: ReturnType<typeof grokPolicy>, args: readonly string[], home: string): {
|
|
26
|
+
args: string[];
|
|
27
|
+
start: boolean;
|
|
28
|
+
receive: boolean;
|
|
29
|
+
reason: string;
|
|
30
|
+
} | {
|
|
31
|
+
args?: undefined;
|
|
32
|
+
start: boolean;
|
|
33
|
+
receive: boolean;
|
|
34
|
+
reason: string;
|
|
35
|
+
} | {
|
|
36
|
+
args: string[];
|
|
37
|
+
start: boolean;
|
|
38
|
+
receive: boolean;
|
|
39
|
+
} | {
|
|
40
|
+
args?: undefined;
|
|
41
|
+
start: boolean;
|
|
42
|
+
receive: boolean;
|
|
43
|
+
};
|
|
44
|
+
/** The whole file setup writes; teardown removes it only while it is still exactly this. */
|
|
45
|
+
export declare function grokHookFileText(): string;
|
|
46
|
+
export declare const grokHookSetup: QuotaSetup;
|
|
47
|
+
export declare const grokHandoff: FamilyHandoff;
|