@inetafrica/open-claudia 2.6.15 → 2.6.21
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/CHANGELOG.md +3 -0
- package/bot-agent.js +5 -3
- package/bot.js +2 -1
- package/core/config.js +48 -23
- package/core/handlers.js +8 -3
- package/core/runner.js +158 -53
- package/core/state.js +4 -1
- package/core/system-prompt.js +9 -3
- package/core/version.js +22 -0
- package/package.json +4 -3
- package/test-usage-accounting.js +84 -0
- package/web.js +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.20
|
|
4
|
+
- **AgentSpace pod durability.** `/upgrade` and password-change callbacks now preserve the `/api` base path expected by AgentSpace, CLI paths are resolved to concrete executables at startup, and runs repair a stale/missing session workspace before spawning Claude, Cursor, or Codex. This prevents misleading `spawn ... ENOENT` failures when the saved session cwd no longer exists.
|
|
5
|
+
|
|
3
6
|
## v2.6.15
|
|
4
7
|
- **Tokenomics guardrails.** The Usage dashboard now shows the latest context-token count, recent baseline, current rate multiplier, and active alert policy above the existing per-version/token trend charts. Completed turns already logged to `~/.open-claudia/usage-history.jsonl`; the dashboard now uses that history to make regressions visible immediately after `/upgrade`.
|
|
5
8
|
- **Hard memory recall budget.** Auto-recalled packs/entities now share a total `MEMORY_RECALL_MAX_CHARS` budget (default `9000`) after relevance filtering. Matched memory beyond the budget is omitted with a short pointer to inspect packs/entities manually, so long-term memory remains available without silently bloating every turn.
|
package/bot-agent.js
CHANGED
|
@@ -150,6 +150,7 @@ bot.on("polling_error", (err) => {
|
|
|
150
150
|
|
|
151
151
|
// ── Update checker (every 5 mins) ──────────────────────────────────
|
|
152
152
|
const CURRENT_VERSION = require(path.join(__dirname, "package.json")).version;
|
|
153
|
+
const { isNewerVersion } = require("./core/version");
|
|
153
154
|
let lastNotifiedVersion = null;
|
|
154
155
|
|
|
155
156
|
function checkForUpdates() {
|
|
@@ -159,7 +160,7 @@ function checkForUpdates() {
|
|
|
159
160
|
res.on("end", () => {
|
|
160
161
|
try {
|
|
161
162
|
const latest = JSON.parse(data).version;
|
|
162
|
-
if (latest
|
|
163
|
+
if (isNewerVersion(latest, CURRENT_VERSION) && latest !== lastNotifiedVersion) {
|
|
163
164
|
lastNotifiedVersion = latest;
|
|
164
165
|
bot.sendMessage(CHAT_ID, `Hey! A new version is available (v${latest}). You're on v${CURRENT_VERSION}.\n\nSend /upgrade to update — I'll be back in a few seconds.`);
|
|
165
166
|
}
|
|
@@ -1585,12 +1586,13 @@ bot.onText(/\/upgrade$/, async (msg) => {
|
|
|
1585
1586
|
if (!isOwner(msg)) return;
|
|
1586
1587
|
try { process.chdir(process.env.HOME || require("os").homedir()); } catch (e) { /* already gone */ }
|
|
1587
1588
|
// Check if there's actually a newer version
|
|
1589
|
+
let latest = null;
|
|
1588
1590
|
try {
|
|
1589
|
-
|
|
1591
|
+
latest = execSync("npm view @inetafrica/open-claudia version", {
|
|
1590
1592
|
encoding: "utf-8", timeout: 15000,
|
|
1591
1593
|
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
|
|
1592
1594
|
}).trim();
|
|
1593
|
-
if (latest
|
|
1595
|
+
if (!isNewerVersion(latest, CURRENT_VERSION)) {
|
|
1594
1596
|
await send(`Already on the latest version (v${CURRENT_VERSION}).`);
|
|
1595
1597
|
return;
|
|
1596
1598
|
}
|
package/bot.js
CHANGED
|
@@ -16,6 +16,7 @@ const { isOnboarded } = require("./core/onboarding");
|
|
|
16
16
|
const { initScheduler, stopAll: stopScheduler } = require("./core/scheduler");
|
|
17
17
|
const { onMessage, onAction } = require("./core/router");
|
|
18
18
|
const { publicCommands } = require("./core/commands");
|
|
19
|
+
const { isNewerVersion } = require("./core/version");
|
|
19
20
|
const registry = require("./core/adapter-registry");
|
|
20
21
|
const loopback = require("./core/loopback");
|
|
21
22
|
require("./core/handlers"); // side-effect: register slash commands
|
|
@@ -102,7 +103,7 @@ function checkForUpdates() {
|
|
|
102
103
|
res.on("end", () => {
|
|
103
104
|
try {
|
|
104
105
|
const latest = JSON.parse(data).version;
|
|
105
|
-
if (latest
|
|
106
|
+
if (isNewerVersion(latest, CURRENT_VERSION) && latest !== lastNotifiedVersion) {
|
|
106
107
|
lastNotifiedVersion = latest;
|
|
107
108
|
// Notify owner via the first telegram adapter (the historical home).
|
|
108
109
|
const tg = adapters.find((a) => a.type === "telegram");
|
package/core/config.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
-
const {
|
|
6
|
+
const { execFileSync } = require("child_process");
|
|
7
7
|
const CONFIG_DIR = require("../config-dir");
|
|
8
8
|
const { truthy: configTruthy } = require("../project-transcripts");
|
|
9
9
|
|
|
@@ -12,11 +12,12 @@ const ENV_PATH = path.join(CONFIG_DIR, ".env");
|
|
|
12
12
|
|
|
13
13
|
function loadEnv() {
|
|
14
14
|
if (!fs.existsSync(ENV_PATH)) {
|
|
15
|
+
if (process.env.OPEN_CLAUDIA_TEST === "1") return { ...process.env };
|
|
15
16
|
console.error("No .env file found. Run: node setup.js");
|
|
16
17
|
process.exit(1);
|
|
17
18
|
}
|
|
18
19
|
const lines = fs.readFileSync(ENV_PATH, "utf-8").split("\n");
|
|
19
|
-
const env = {};
|
|
20
|
+
const env = { ...process.env };
|
|
20
21
|
for (const line of lines) {
|
|
21
22
|
const idx = line.indexOf("=");
|
|
22
23
|
if (idx > 0) env[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
@@ -38,13 +39,55 @@ function saveEnvKey(key, value) {
|
|
|
38
39
|
|
|
39
40
|
const config = loadEnv();
|
|
40
41
|
|
|
42
|
+
function hasPathSeparator(value) {
|
|
43
|
+
return value.includes("/") || value.includes("\\");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function which(command) {
|
|
47
|
+
const lookup = process.platform === "win32" ? "where" : "which";
|
|
48
|
+
return execFileSync(lookup, [command], { encoding: "utf-8" })
|
|
49
|
+
.split(/\r?\n/)
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter(Boolean)[0] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveExecutablePath(configured, fallbackCommand, label, opts = {}) {
|
|
55
|
+
const raw = (configured || fallbackCommand || "").trim();
|
|
56
|
+
if (!raw) {
|
|
57
|
+
if (opts.required) {
|
|
58
|
+
console.error(`${label} not set`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (path.isAbsolute(raw) || hasPathSeparator(raw)) {
|
|
65
|
+
if (fs.existsSync(raw)) return raw;
|
|
66
|
+
if (opts.required) {
|
|
67
|
+
console.error(`${label} not found at: ${raw}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return which(raw);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (opts.required) {
|
|
77
|
+
console.error(`${label} not found on PATH: ${raw}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
// Telegram (kept for backwards compat — channels list defaults to telegram)
|
|
42
85
|
const TOKEN = config.TELEGRAM_BOT_TOKEN;
|
|
43
86
|
const CHAT_IDS = (config.TELEGRAM_CHAT_ID || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
44
87
|
const CHAT_ID = CHAT_IDS[0];
|
|
45
88
|
|
|
46
89
|
const WORKSPACE = config.WORKSPACE;
|
|
47
|
-
const CLAUDE_PATH = config.CLAUDE_PATH;
|
|
90
|
+
const CLAUDE_PATH = resolveExecutablePath(config.CLAUDE_PATH, null, "Claude CLI", { required: true });
|
|
48
91
|
const CURSOR_PATH = config.CURSOR_PATH || null;
|
|
49
92
|
const CODEX_PATH = config.CODEX_PATH || null;
|
|
50
93
|
const DEFAULT_CLAUDE_MODEL = config.CLAUDE_MODEL || process.env.CLAUDE_MODEL || "claude-fable-5";
|
|
@@ -78,7 +121,6 @@ const MAX_PROCESS_TIMEOUT = 360 * 60 * 1000;
|
|
|
78
121
|
const COMPACT_SUMMARY_TIMEOUT = 10 * 60 * 1000;
|
|
79
122
|
|
|
80
123
|
if (!WORKSPACE) { console.error("WORKSPACE not set"); process.exit(1); }
|
|
81
|
-
if (!CLAUDE_PATH) { console.error("CLAUDE_PATH not set"); process.exit(1); }
|
|
82
124
|
|
|
83
125
|
if (!fs.existsSync(WORKSPACE)) {
|
|
84
126
|
try {
|
|
@@ -90,27 +132,10 @@ if (!fs.existsSync(WORKSPACE)) {
|
|
|
90
132
|
}
|
|
91
133
|
}
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
execSync(`which "${CLAUDE_PATH}" 2>/dev/null || where "${CLAUDE_PATH}" 2>nul`, { encoding: "utf-8" });
|
|
96
|
-
} catch (e) {
|
|
97
|
-
console.error(`Claude CLI not found at: ${CLAUDE_PATH}`);
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let resolvedCursorPath = CURSOR_PATH;
|
|
103
|
-
if (!resolvedCursorPath) {
|
|
104
|
-
try { resolvedCursorPath = execSync("which agent 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
|
|
105
|
-
catch (e) { resolvedCursorPath = null; }
|
|
106
|
-
}
|
|
135
|
+
let resolvedCursorPath = resolveExecutablePath(CURSOR_PATH, "agent", "Cursor Agent CLI");
|
|
107
136
|
if (resolvedCursorPath) console.error(`Cursor Agent CLI: ${resolvedCursorPath}`);
|
|
108
137
|
|
|
109
|
-
let resolvedCodexPath = CODEX_PATH;
|
|
110
|
-
if (!resolvedCodexPath) {
|
|
111
|
-
try { resolvedCodexPath = execSync("which codex 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
|
|
112
|
-
catch (e) { resolvedCodexPath = null; }
|
|
113
|
-
}
|
|
138
|
+
let resolvedCodexPath = resolveExecutablePath(CODEX_PATH, "codex", "Codex CLI");
|
|
114
139
|
if (resolvedCodexPath) console.error(`Codex CLI: ${resolvedCodexPath}`);
|
|
115
140
|
|
|
116
141
|
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
package/core/handlers.js
CHANGED
|
@@ -21,6 +21,7 @@ const { listProjects, findProject, projectKeyboard, workspacePath } = require(".
|
|
|
21
21
|
const { vault } = require("./vault-store");
|
|
22
22
|
const { redactSensitive } = require("./redact");
|
|
23
23
|
const { runDoctorChecks, formatDoctorReport } = require("./doctor");
|
|
24
|
+
const { isNewerVersion } = require("./version");
|
|
24
25
|
const jobs = require("./jobs");
|
|
25
26
|
const scheduler = require("./scheduler");
|
|
26
27
|
const skillsLib = require("./skills");
|
|
@@ -338,7 +339,10 @@ async function requestAgentSpaceUpgrade() {
|
|
|
338
339
|
const apiUrl = process.env.AGENTSPACE_API_URL;
|
|
339
340
|
const token = process.env.AGENTSPACE_POD_TOKEN;
|
|
340
341
|
if (!apiUrl || !token) return null;
|
|
341
|
-
const
|
|
342
|
+
const base = new URL(apiUrl);
|
|
343
|
+
if (base.pathname === "/") base.pathname = "/api/";
|
|
344
|
+
if (!base.pathname.endsWith("/")) base.pathname += "/";
|
|
345
|
+
const u = new URL("pods/self/upgrade", base);
|
|
342
346
|
const lib = u.protocol === "https:" ? require("https") : require("http");
|
|
343
347
|
return new Promise((resolve) => {
|
|
344
348
|
const req = lib.request({
|
|
@@ -434,7 +438,7 @@ register({
|
|
|
434
438
|
await send(`Upgrade failed: could not query npm registry (${(e.message || String(e)).slice(0, 200)}).`);
|
|
435
439
|
return;
|
|
436
440
|
}
|
|
437
|
-
if (latest
|
|
441
|
+
if (!isNewerVersion(latest, CURRENT_VERSION)) {
|
|
438
442
|
await send(`Already on the latest version (v${CURRENT_VERSION}).`);
|
|
439
443
|
return;
|
|
440
444
|
}
|
|
@@ -474,7 +478,7 @@ register({
|
|
|
474
478
|
encoding: "utf-8", timeout: 15000,
|
|
475
479
|
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
|
|
476
480
|
}).trim();
|
|
477
|
-
if (latest
|
|
481
|
+
if (!isNewerVersion(latest, CURRENT_VERSION)) {
|
|
478
482
|
await send(`Already on the latest version (v${CURRENT_VERSION}).`);
|
|
479
483
|
return;
|
|
480
484
|
}
|
|
@@ -594,6 +598,7 @@ register({
|
|
|
594
598
|
if (resolvedCodexPath) {
|
|
595
599
|
rows.push([{ text: "── Codex ──", callback_data: "noop" }]);
|
|
596
600
|
rows.push([
|
|
601
|
+
{ text: "GPT-5.5", callback_data: "mb:codex:gpt-5.5" },
|
|
597
602
|
{ text: "gpt-5", callback_data: "mb:codex:gpt-5" },
|
|
598
603
|
{ text: "gpt-5-codex", callback_data: "mb:codex:gpt-5-codex" },
|
|
599
604
|
]);
|
package/core/runner.js
CHANGED
|
@@ -9,9 +9,9 @@ const { spawn, execFileSync } = require("child_process");
|
|
|
9
9
|
const {
|
|
10
10
|
CLAUDE_PATH, DEFAULT_CLAUDE_MODEL, resolvedCursorPath, resolvedCodexPath,
|
|
11
11
|
AUTO_COMPACT_TOKENS, MIN_COMPACT_INTERVAL_MS, MAX_PROCESS_TIMEOUT, COMPACT_SUMMARY_TIMEOUT, botSubprocessEnv,
|
|
12
|
-
CONFIG_DIR, config,
|
|
12
|
+
CONFIG_DIR, WORKSPACE, config,
|
|
13
13
|
} = require("./config");
|
|
14
|
-
const { currentState, saveState, recordSession, userOwnsClaudeSession } = require("./state");
|
|
14
|
+
const { currentState, saveState, recordSession, userOwnsClaudeSession, resetSessionUsage } = require("./state");
|
|
15
15
|
const { chatContext, currentChannelId, currentAdapter } = require("./context");
|
|
16
16
|
const { buildSystemPrompt, promptWithDynamicContext } = require("./system-prompt");
|
|
17
17
|
const { redactSensitive } = require("./redact");
|
|
@@ -20,7 +20,7 @@ const { textToVoice, TTS_CMD } = require("./media");
|
|
|
20
20
|
const { killProcessTree } = require("./process-tree");
|
|
21
21
|
const {
|
|
22
22
|
appendProjectTranscript, transcriptProjectInfo,
|
|
23
|
-
|
|
23
|
+
stripTranscriptPointerForStorage,
|
|
24
24
|
} = require("./transcripts");
|
|
25
25
|
const { getClaudeOAuthToken, claudeAuthRecoveryMessage, isClaudeAuthErrorText, claudeUsageLimitMessage, isClaudeUsageLimitText, runClaudeAuthStatusDiagnostic, claudeSubprocessEnv } = require("./auth-flow");
|
|
26
26
|
const loopback = require("./loopback");
|
|
@@ -45,13 +45,91 @@ function fmtTokens(n) {
|
|
|
45
45
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
48
|
+
function usageNumber(usage, ...keys) {
|
|
49
|
+
for (const key of keys) {
|
|
50
|
+
const value = usage?.[key];
|
|
51
|
+
if (Number.isFinite(value)) return value;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function usageParts(usage, backend = "claude") {
|
|
57
|
+
const input = usageNumber(usage, "input_tokens");
|
|
58
|
+
const cacheRead = backend === "codex"
|
|
59
|
+
? usageNumber(usage, "cached_input_tokens", "cache_read_input_tokens")
|
|
60
|
+
: usageNumber(usage, "cache_read_input_tokens", "cached_input_tokens");
|
|
61
|
+
const cacheCreation = usageNumber(usage, "cache_creation_input_tokens");
|
|
62
|
+
const output = usageNumber(usage, "output_tokens");
|
|
63
|
+
return {
|
|
64
|
+
input,
|
|
65
|
+
cacheRead,
|
|
66
|
+
cacheCreation,
|
|
67
|
+
output,
|
|
68
|
+
// Codex/OpenAI reports cached input as part of input_tokens. Keep the
|
|
69
|
+
// cache-read field for attribution, but do not add it again to context.
|
|
70
|
+
context: backend === "codex" ? input : input + cacheRead + cacheCreation,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function clampUsageDelta(current, previous) {
|
|
75
|
+
if (!previous) return current;
|
|
76
|
+
return {
|
|
77
|
+
input_tokens: Math.max(0, usageNumber(current, "input_tokens") - usageNumber(previous, "input_tokens")),
|
|
78
|
+
cached_input_tokens: Math.max(0, usageNumber(current, "cached_input_tokens") - usageNumber(previous, "cached_input_tokens")),
|
|
79
|
+
cache_read_input_tokens: Math.max(0, usageNumber(current, "cache_read_input_tokens") - usageNumber(previous, "cache_read_input_tokens")),
|
|
80
|
+
cache_creation_input_tokens: Math.max(0, usageNumber(current, "cache_creation_input_tokens") - usageNumber(previous, "cache_creation_input_tokens")),
|
|
81
|
+
output_tokens: Math.max(0, usageNumber(current, "output_tokens") - usageNumber(previous, "output_tokens")),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function snapshotCodexUsage(state, sessionId, usage) {
|
|
86
|
+
if (!state || !sessionId || !usage) return null;
|
|
87
|
+
const snapshots = state.codexUsageSnapshots && typeof state.codexUsageSnapshots === "object"
|
|
88
|
+
? state.codexUsageSnapshots
|
|
89
|
+
: {};
|
|
90
|
+
const previous = snapshots[sessionId] || null;
|
|
91
|
+
snapshots[sessionId] = {
|
|
92
|
+
input_tokens: usageNumber(usage, "input_tokens"),
|
|
93
|
+
cached_input_tokens: usageNumber(usage, "cached_input_tokens"),
|
|
94
|
+
output_tokens: usageNumber(usage, "output_tokens"),
|
|
95
|
+
};
|
|
96
|
+
const keys = Object.keys(snapshots);
|
|
97
|
+
if (keys.length > 20) {
|
|
98
|
+
for (const key of keys.slice(0, keys.length - 20)) delete snapshots[key];
|
|
99
|
+
}
|
|
100
|
+
state.codexUsageSnapshots = snapshots;
|
|
101
|
+
return previous;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeCodexUsage(state, sessionId, usage) {
|
|
105
|
+
const previous = snapshotCodexUsage(state, sessionId, usage);
|
|
106
|
+
if (!previous) return { usage, usageScope: "session_total", rawUsage: usage };
|
|
107
|
+
return { usage: clampUsageDelta(usage, previous), usageScope: "turn_delta", rawUsage: usage };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function applyUsageToState(state, usage, costUsd, opts = {}) {
|
|
111
|
+
if (!usage) return null;
|
|
112
|
+
const backend = state.settings?.backend || "claude";
|
|
113
|
+
const parts = usageParts(usage, backend);
|
|
114
|
+
const u = state.sessionUsage;
|
|
115
|
+
u.turns += 1;
|
|
116
|
+
u.inputTokens += parts.input;
|
|
117
|
+
u.outputTokens += parts.output;
|
|
118
|
+
u.cacheReadTokens += parts.cacheRead;
|
|
119
|
+
u.cacheCreationTokens += parts.cacheCreation;
|
|
120
|
+
u.lastInputTokens = parts.context;
|
|
121
|
+
u.lastUsageScope = opts.usageScope || "turn_delta";
|
|
122
|
+
if (Number.isFinite(opts.liveContextTokens)) u.liveContextTokens = opts.liveContextTokens;
|
|
123
|
+
else u.liveContextTokens = parts.context;
|
|
124
|
+
if (typeof costUsd === "number") u.costUsd += costUsd;
|
|
125
|
+
return parts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
|
|
49
129
|
if (!usage) return null;
|
|
50
130
|
const backend = state.settings?.backend || "claude";
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const cacheCreation = usage.cache_creation_input_tokens || 0;
|
|
54
|
-
const output = usage.output_tokens || 0;
|
|
131
|
+
const parts = usageParts(usage, backend);
|
|
132
|
+
const usageScope = opts.usageScope || "turn_delta";
|
|
55
133
|
const record = {
|
|
56
134
|
ts: new Date().toISOString(),
|
|
57
135
|
version: PKG_VERSION,
|
|
@@ -59,13 +137,21 @@ function logTurnUsage(state, usage, costUsd, announce) {
|
|
|
59
137
|
model: state.settings?.model || (backend === "claude" ? DEFAULT_CLAUDE_MODEL : null),
|
|
60
138
|
userId: state.userId || null,
|
|
61
139
|
sessionId: state[getActiveSessionKey(state)] || null,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
140
|
+
usageScope,
|
|
141
|
+
inputTokens: parts.input,
|
|
142
|
+
outputTokens: parts.output,
|
|
143
|
+
cacheReadTokens: parts.cacheRead,
|
|
144
|
+
cacheCreationTokens: parts.cacheCreation,
|
|
145
|
+
contextTokens: parts.context,
|
|
67
146
|
costUsd: typeof costUsd === "number" ? costUsd : 0,
|
|
68
147
|
};
|
|
148
|
+
if (opts.rawUsage && opts.rawUsage !== usage) {
|
|
149
|
+
const raw = usageParts(opts.rawUsage, backend);
|
|
150
|
+
record.rawInputTokens = raw.input;
|
|
151
|
+
record.rawOutputTokens = raw.output;
|
|
152
|
+
record.rawCacheReadTokens = raw.cacheRead;
|
|
153
|
+
record.rawContextTokens = raw.context;
|
|
154
|
+
}
|
|
69
155
|
const previousRecords = loadUsageHistory();
|
|
70
156
|
appendUsageRecord(record);
|
|
71
157
|
|
|
@@ -77,9 +163,10 @@ function logTurnUsage(state, usage, costUsd, announce) {
|
|
|
77
163
|
if (announce) {
|
|
78
164
|
const baseline = result.trend?.baselineAvgContextTokens || 0;
|
|
79
165
|
const rate = result.trend?.latestRate || 0;
|
|
166
|
+
const contextLabel = usageScope === "turn_delta" ? "Context this turn" : "Session context total";
|
|
80
167
|
announce([
|
|
81
168
|
"Token usage alert:",
|
|
82
|
-
|
|
169
|
+
`${contextLabel}: ${fmtTokens(record.contextTokens)}`,
|
|
83
170
|
baseline ? `Recent baseline: ${fmtTokens(baseline)} (${rate.toFixed(2)}x)` : null,
|
|
84
171
|
`Version/model: v${record.version} / ${record.model || record.backend}`,
|
|
85
172
|
`Reason: ${result.reasons.join("; ")}`,
|
|
@@ -154,6 +241,26 @@ function getActiveSessionKey(state = currentState()) {
|
|
|
154
241
|
return "lastSessionId";
|
|
155
242
|
}
|
|
156
243
|
|
|
244
|
+
function resolveRunCwd(cwd) {
|
|
245
|
+
const state = currentState();
|
|
246
|
+
const requested = cwd || state.currentSession?.dir || WORKSPACE || process.cwd();
|
|
247
|
+
if (requested && fs.existsSync(requested)) return requested;
|
|
248
|
+
|
|
249
|
+
const fallback = WORKSPACE && fs.existsSync(WORKSPACE) ? WORKSPACE : process.cwd();
|
|
250
|
+
if (state.currentSession && state.currentSession.dir === requested) {
|
|
251
|
+
console.warn(`[session-guard] repairing missing session dir ${requested} -> ${fallback}`);
|
|
252
|
+
state.currentSession = { ...state.currentSession, dir: fallback };
|
|
253
|
+
state.lastSessionId = null;
|
|
254
|
+
state.cursorSessionId = null;
|
|
255
|
+
state.codexSessionId = null;
|
|
256
|
+
resetSessionUsage(state);
|
|
257
|
+
saveState();
|
|
258
|
+
} else {
|
|
259
|
+
console.warn(`[session-guard] missing run cwd ${requested}; using ${fallback}`);
|
|
260
|
+
}
|
|
261
|
+
return fallback;
|
|
262
|
+
}
|
|
263
|
+
|
|
157
264
|
function effectiveCompactThreshold(state = currentState()) {
|
|
158
265
|
const override = state.settings?.compactWindow;
|
|
159
266
|
if (override === 0) return 0;
|
|
@@ -166,9 +273,11 @@ function shouldAutoCompact(state = currentState(), opts = {}) {
|
|
|
166
273
|
if (!state[getActiveSessionKey(state)]) return false;
|
|
167
274
|
const threshold = effectiveCompactThreshold(state);
|
|
168
275
|
if (threshold === 0) return false;
|
|
169
|
-
|
|
276
|
+
const contextTokens = state.sessionUsage?.liveContextTokens || state.sessionUsage?.lastInputTokens || 0;
|
|
277
|
+
if (contextTokens < threshold) return false;
|
|
170
278
|
const minInterval = Number.isFinite(MIN_COMPACT_INTERVAL_MS) ? MIN_COMPACT_INTERVAL_MS : 1800000;
|
|
171
|
-
|
|
279
|
+
const farOverThreshold = contextTokens >= threshold * 2;
|
|
280
|
+
if (!farOverThreshold && state.lastCompactedAt && (Date.now() - state.lastCompactedAt) < minInterval) return false;
|
|
172
281
|
return true;
|
|
173
282
|
}
|
|
174
283
|
|
|
@@ -219,7 +328,7 @@ function buildCursorArgs(prompt, opts = {}) {
|
|
|
219
328
|
return args;
|
|
220
329
|
}
|
|
221
330
|
|
|
222
|
-
function buildCodexArgs(prompt, opts = {}) {
|
|
331
|
+
async function buildCodexArgs(prompt, opts = {}) {
|
|
223
332
|
const state = currentState();
|
|
224
333
|
const { settings, codexSessionId } = state;
|
|
225
334
|
const args = [];
|
|
@@ -231,7 +340,7 @@ function buildCodexArgs(prompt, opts = {}) {
|
|
|
231
340
|
if (settings.permissionMode === "plan") args.push("--sandbox", "read-only");
|
|
232
341
|
else args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
233
342
|
if (settings.model) args.push("--model", settings.model);
|
|
234
|
-
args.push(
|
|
343
|
+
args.push(await promptWithDynamicContext(prompt, { includeTranscriptPointer: true }));
|
|
235
344
|
return args;
|
|
236
345
|
}
|
|
237
346
|
|
|
@@ -504,6 +613,7 @@ async function runClaudeCapture(prompt, cwd, opts = {}) {
|
|
|
504
613
|
if (state.runningProcess) throw new Error("Another task is already running.");
|
|
505
614
|
const authPreflight = preflightClaudeAuthMessage();
|
|
506
615
|
if (authPreflight) throw new Error(authPreflight);
|
|
616
|
+
cwd = resolveRunCwd(cwd);
|
|
507
617
|
|
|
508
618
|
const args = await buildClaudeArgs(prompt, { ...opts, skipAutoCompact: true });
|
|
509
619
|
return new Promise((resolve, reject) => {
|
|
@@ -556,15 +666,7 @@ async function runClaudeCapture(prompt, cwd, opts = {}) {
|
|
|
556
666
|
else state.lastSessionId = evt.session_id;
|
|
557
667
|
sessionId = evt.session_id;
|
|
558
668
|
if (evt.usage) {
|
|
559
|
-
|
|
560
|
-
u.turns += 1;
|
|
561
|
-
u.inputTokens += evt.usage.input_tokens || 0;
|
|
562
|
-
u.outputTokens += evt.usage.output_tokens || 0;
|
|
563
|
-
u.cacheReadTokens += evt.usage.cache_read_input_tokens || 0;
|
|
564
|
-
u.cacheCreationTokens += evt.usage.cache_creation_input_tokens || 0;
|
|
565
|
-
u.lastInputTokens = (evt.usage.input_tokens || 0) +
|
|
566
|
-
(evt.usage.cache_read_input_tokens || 0) +
|
|
567
|
-
(evt.usage.cache_creation_input_tokens || 0);
|
|
669
|
+
applyUsageToState(state, evt.usage, 0, { usageScope: "turn_delta" });
|
|
568
670
|
}
|
|
569
671
|
if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
|
|
570
672
|
saveState();
|
|
@@ -630,7 +732,8 @@ async function compactActiveSession(cwd, opts = {}) {
|
|
|
630
732
|
state[sessionKey] = newSessionId;
|
|
631
733
|
state.isFirstMessage = false;
|
|
632
734
|
state.lastCompactedAt = Date.now();
|
|
633
|
-
state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
|
|
735
|
+
state.sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0, liveContextTokens: 0, lastUsageScope: "turn_delta" };
|
|
736
|
+
state.codexUsageSnapshots = {};
|
|
634
737
|
saveState();
|
|
635
738
|
|
|
636
739
|
if (newSessionId && state.currentSession) {
|
|
@@ -646,6 +749,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
646
749
|
const channelId = currentChannelId();
|
|
647
750
|
const adapter = currentAdapter();
|
|
648
751
|
const { settings } = state;
|
|
752
|
+
cwd = resolveRunCwd(cwd);
|
|
649
753
|
|
|
650
754
|
if (state.runningProcess) {
|
|
651
755
|
state.messageQueue.push({ prompt, replyToMsgId, opts, queuedAt: Date.now() });
|
|
@@ -659,6 +763,14 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
659
763
|
return;
|
|
660
764
|
}
|
|
661
765
|
|
|
766
|
+
if (state.currentSession && shouldAutoCompact(state, opts)) {
|
|
767
|
+
try {
|
|
768
|
+
await compactActiveSession(state.currentSession.dir, { notify: true, message: "Context is getting large, compacting before I handle this…" });
|
|
769
|
+
} catch (e) {
|
|
770
|
+
console.warn(`[preflight-compact] failed: ${redactSensitive(e.message)}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
662
774
|
appendProjectTranscript("user", prompt, {
|
|
663
775
|
sourceMessageId: replyToMsgId || null,
|
|
664
776
|
fresh: !!opts.fresh,
|
|
@@ -868,37 +980,26 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
868
980
|
}
|
|
869
981
|
}
|
|
870
982
|
if (evt.type === "turn.completed" && evt.usage && settings.backend === "codex") {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
u.lastInputTokens = input + cached;
|
|
880
|
-
logTurnUsage(state, evt.usage, 0, (text) => chatContext.run(store, () => send(text)));
|
|
983
|
+
const session = state.codexSessionId || evt.thread_id || getActiveSessionId();
|
|
984
|
+
const normalized = normalizeCodexUsage(state, session, evt.usage);
|
|
985
|
+
const rawParts = usageParts(evt.usage, "codex");
|
|
986
|
+
applyUsageToState(state, normalized.usage, 0, {
|
|
987
|
+
usageScope: normalized.usageScope,
|
|
988
|
+
liveContextTokens: rawParts.context,
|
|
989
|
+
});
|
|
990
|
+
logTurnUsage(state, normalized.usage, 0, (text) => chatContext.run(store, () => send(text)), normalized);
|
|
881
991
|
saveState();
|
|
882
992
|
}
|
|
883
993
|
if (evt.type === "result" && evt.session_id) {
|
|
884
994
|
if (settings.backend === "cursor") { state.cursorSessionId = evt.session_id; }
|
|
885
995
|
else { state.lastSessionId = evt.session_id; }
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
u.cacheReadTokens += evt.usage.cache_read_input_tokens || 0;
|
|
892
|
-
u.cacheCreationTokens += evt.usage.cache_creation_input_tokens || 0;
|
|
893
|
-
u.lastInputTokens = (evt.usage.input_tokens || 0) +
|
|
894
|
-
(evt.usage.cache_read_input_tokens || 0) +
|
|
895
|
-
(evt.usage.cache_creation_input_tokens || 0);
|
|
896
|
-
// Codex already logged this turn at turn.completed — don't double-count.
|
|
897
|
-
if (settings.backend !== "codex") {
|
|
898
|
-
logTurnUsage(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, (text) => chatContext.run(store, () => send(text)));
|
|
899
|
-
}
|
|
996
|
+
// Codex already reports usage at turn.completed. A result usage block,
|
|
997
|
+
// if present, is the same cumulative total and must not be counted again.
|
|
998
|
+
if (evt.usage && settings.backend !== "codex") {
|
|
999
|
+
applyUsageToState(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, { usageScope: "turn_delta" });
|
|
1000
|
+
logTurnUsage(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, (text) => chatContext.run(store, () => send(text)), { usageScope: "turn_delta" });
|
|
900
1001
|
}
|
|
901
|
-
if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
|
|
1002
|
+
if (typeof evt.total_cost_usd === "number" && settings.backend === "codex") state.sessionUsage.costUsd += evt.total_cost_usd;
|
|
902
1003
|
saveState();
|
|
903
1004
|
}
|
|
904
1005
|
// Fallback only: evt.result is just the final text segment, and assigning
|
|
@@ -1068,6 +1169,10 @@ module.exports = {
|
|
|
1068
1169
|
getActiveSessionKey,
|
|
1069
1170
|
shouldAutoCompact,
|
|
1070
1171
|
effectiveCompactThreshold,
|
|
1172
|
+
usageParts,
|
|
1173
|
+
clampUsageDelta,
|
|
1174
|
+
normalizeCodexUsage,
|
|
1175
|
+
applyUsageToState,
|
|
1071
1176
|
compactSeedPrompt,
|
|
1072
1177
|
splitCompactionBrief,
|
|
1073
1178
|
archiveCompactionBrief,
|
package/core/state.js
CHANGED
|
@@ -47,7 +47,7 @@ function freshSettings() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function freshUsage() {
|
|
50
|
-
return { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
|
|
50
|
+
return { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0, liveContextTokens: 0, lastUsageScope: "turn_delta" };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function createUserState(userId) {
|
|
@@ -65,6 +65,7 @@ function createUserState(userId) {
|
|
|
65
65
|
lastSessionId: saved.lastSessionId || null,
|
|
66
66
|
cursorSessionId: saved.cursorSessionId || null,
|
|
67
67
|
codexSessionId: saved.codexSessionId || null,
|
|
68
|
+
codexUsageSnapshots: saved.codexUsageSnapshots || {},
|
|
68
69
|
messageQueue: [],
|
|
69
70
|
isFirstMessage: !saved.lastSessionId,
|
|
70
71
|
lastInputWasVoice: false,
|
|
@@ -97,6 +98,7 @@ function currentState() {
|
|
|
97
98
|
|
|
98
99
|
function resetSessionUsage(state = currentState()) {
|
|
99
100
|
state.sessionUsage = freshUsage();
|
|
101
|
+
state.codexUsageSnapshots = {};
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
function resetSettings(state = currentState()) {
|
|
@@ -111,6 +113,7 @@ function saveState() {
|
|
|
111
113
|
lastSessionId: s.lastSessionId,
|
|
112
114
|
cursorSessionId: s.cursorSessionId,
|
|
113
115
|
codexSessionId: s.codexSessionId,
|
|
116
|
+
codexUsageSnapshots: s.codexUsageSnapshots || {},
|
|
114
117
|
settings: s.settings,
|
|
115
118
|
sessionUsage: s.sessionUsage,
|
|
116
119
|
lastCompactedAt: s.lastCompactedAt || 0,
|
package/core/system-prompt.js
CHANGED
|
@@ -118,6 +118,11 @@ ${buildPersonaBlock()}
|
|
|
118
118
|
- Voice notes: ${hasVoice ? "enabled" : "disabled"}
|
|
119
119
|
- Vault status, session state, and any pending tasks arrive in a "Runtime state" block at the top of each user message, not here.
|
|
120
120
|
|
|
121
|
+
## Open Claudia Skills
|
|
122
|
+
Open Claudia learned skills are stored as context packs under ${path.join(CONFIG_DIR, "packs")}. Older \`~/.claude/skills/<name>/SKILL.md\` skills may have been migrated into packs; their reusable instructions live in the pack's Procedure section.
|
|
123
|
+
|
|
124
|
+
If the user asks for a skill by name, do not rely only on the backend harness's native "Available skills" list. First use any Active context pack injected into the current request as the requested Open Claudia skill. If no matching pack was injected, inspect with \`open-claudia pack list\` / \`open-claudia pack show <dir>\` and legacy \`/skills\` paths before saying the skill does not exist.
|
|
125
|
+
|
|
121
126
|
## Stable Local Paths
|
|
122
127
|
- Bot code: ${path.join(BOT_DIR, "bot.js")}
|
|
123
128
|
- Soul file (identity + hard rules): ${SOUL_FILE}
|
|
@@ -379,7 +384,7 @@ function buildPackBlock(matches, budget) {
|
|
|
379
384
|
}
|
|
380
385
|
if (used.length > 0) setImmediate(() => { try { packsLib.touchUsed(used); } catch (e) {} });
|
|
381
386
|
if (blocks.length === 0) return "";
|
|
382
|
-
return `\n\n## Active context packs\nLong-term topic context auto-matched to this message. Source files live under ${packsLib.PACKS_DIR}/<dir>/PACK.md — read or edit them directly when deeper context or a correction is needed.\n\n${blocks.join("\n\n---\n\n")}`;
|
|
387
|
+
return `\n\n## Active Open Claudia skills / context packs\nLong-term topic context auto-matched to this message. If the user asked for a skill by name, treat the matching pack's Procedure section as that Open Claudia skill. Source files live under ${packsLib.PACKS_DIR}/<dir>/PACK.md — read or edit them directly when deeper context or a correction is needed.\n\n${blocks.join("\n\n---\n\n")}`;
|
|
383
388
|
} catch (e) {
|
|
384
389
|
return "";
|
|
385
390
|
}
|
|
@@ -518,7 +523,7 @@ function logRecall(msg, candPacks, candEntities, keptPacks, keptEntities) {
|
|
|
518
523
|
} catch (e) {}
|
|
519
524
|
}
|
|
520
525
|
|
|
521
|
-
async function promptWithDynamicContext(prompt) {
|
|
526
|
+
async function promptWithDynamicContext(prompt, opts = {}) {
|
|
522
527
|
lastInjected = { packs: [], entities: [] };
|
|
523
528
|
try {
|
|
524
529
|
const { userText, contextText } = recallMatchParts(prompt);
|
|
@@ -557,7 +562,8 @@ async function promptWithDynamicContext(prompt) {
|
|
|
557
562
|
const budgetNote = budget.omitted > 0
|
|
558
563
|
? `\n\n## Memory budget\n${budget.omitted} matched memory item${budget.omitted === 1 ? " was" : "s were"} omitted to keep this turn under the recall budget (${budget.maxChars} chars). Use \`open-claudia pack show <dir>\`, \`entity show <slug>\`, or transcript search if deeper context is needed.`
|
|
559
564
|
: "";
|
|
560
|
-
|
|
565
|
+
const transcriptPointer = opts.includeTranscriptPointer ? `${transcriptPointerNote(state)}\n\n` : "";
|
|
566
|
+
return `${transcriptPointer}${buildDynamicContextBlock()}${packBlock}${entityBlock}${budgetNote}\n\nCurrent user request:\n${prompt}`;
|
|
561
567
|
} catch (e) {
|
|
562
568
|
return prompt;
|
|
563
569
|
}
|
package/core/version.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function parseVersion(version) {
|
|
2
|
+
const match = String(version || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
3
|
+
if (!match) return null;
|
|
4
|
+
return match.slice(1).map((part) => Number.parseInt(part, 10));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function compareVersions(left, right) {
|
|
8
|
+
const a = parseVersion(left);
|
|
9
|
+
const b = parseVersion(right);
|
|
10
|
+
if (!a || !b) return 0;
|
|
11
|
+
for (let i = 0; i < 3; i += 1) {
|
|
12
|
+
if (a[i] > b[i]) return 1;
|
|
13
|
+
if (a[i] < b[i]) return -1;
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isNewerVersion(candidate, current) {
|
|
19
|
+
return compareVersions(candidate, current) > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { compareVersions, isNewerVersion };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.21",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "node -e \"require('./vault'); console.log('OK')\""
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
".dockerignore",
|
|
30
30
|
".env.example",
|
|
31
31
|
"README.md",
|
|
32
|
-
"CHANGELOG.md"
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
"test-usage-accounting.js"
|
|
33
34
|
],
|
|
34
35
|
"keywords": [
|
|
35
36
|
"claude",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const assert = require("assert");
|
|
2
|
+
const {
|
|
3
|
+
usageParts,
|
|
4
|
+
normalizeCodexUsage,
|
|
5
|
+
applyUsageToState,
|
|
6
|
+
shouldAutoCompact,
|
|
7
|
+
} = require("./core/runner");
|
|
8
|
+
|
|
9
|
+
function state(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
settings: { backend: "codex", compactWindow: 100 },
|
|
12
|
+
codexSessionId: "codex-session-1",
|
|
13
|
+
codexUsageSnapshots: {},
|
|
14
|
+
sessionUsage: {
|
|
15
|
+
turns: 0,
|
|
16
|
+
inputTokens: 0,
|
|
17
|
+
outputTokens: 0,
|
|
18
|
+
cacheReadTokens: 0,
|
|
19
|
+
cacheCreationTokens: 0,
|
|
20
|
+
costUsd: 0,
|
|
21
|
+
lastInputTokens: 0,
|
|
22
|
+
liveContextTokens: 0,
|
|
23
|
+
lastUsageScope: "turn_delta",
|
|
24
|
+
},
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assert.deepStrictEqual(
|
|
30
|
+
usageParts({ input_tokens: 100, cached_input_tokens: 60, output_tokens: 10 }, "codex"),
|
|
31
|
+
{ input: 100, cacheRead: 60, cacheCreation: 0, output: 10, context: 100 },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
assert.deepStrictEqual(
|
|
35
|
+
usageParts({ input_tokens: 100, cache_read_input_tokens: 60, cache_creation_input_tokens: 20, output_tokens: 10 }, "claude"),
|
|
36
|
+
{ input: 100, cacheRead: 60, cacheCreation: 20, output: 10, context: 180 },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const s = state();
|
|
40
|
+
const first = normalizeCodexUsage(s, "codex-session-1", {
|
|
41
|
+
input_tokens: 1000,
|
|
42
|
+
cached_input_tokens: 700,
|
|
43
|
+
output_tokens: 50,
|
|
44
|
+
});
|
|
45
|
+
assert.strictEqual(first.usageScope, "session_total");
|
|
46
|
+
assert.strictEqual(usageParts(first.usage, "codex").context, 1000);
|
|
47
|
+
|
|
48
|
+
const second = normalizeCodexUsage(s, "codex-session-1", {
|
|
49
|
+
input_tokens: 1250,
|
|
50
|
+
cached_input_tokens: 800,
|
|
51
|
+
output_tokens: 80,
|
|
52
|
+
});
|
|
53
|
+
assert.strictEqual(second.usageScope, "turn_delta");
|
|
54
|
+
assert.deepStrictEqual(second.usage, {
|
|
55
|
+
input_tokens: 250,
|
|
56
|
+
cached_input_tokens: 100,
|
|
57
|
+
cache_read_input_tokens: 0,
|
|
58
|
+
cache_creation_input_tokens: 0,
|
|
59
|
+
output_tokens: 30,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
applyUsageToState(s, second.usage, 0, {
|
|
63
|
+
usageScope: second.usageScope,
|
|
64
|
+
liveContextTokens: usageParts(second.rawUsage, "codex").context,
|
|
65
|
+
});
|
|
66
|
+
assert.strictEqual(s.sessionUsage.turns, 1);
|
|
67
|
+
assert.strictEqual(s.sessionUsage.inputTokens, 250);
|
|
68
|
+
assert.strictEqual(s.sessionUsage.cacheReadTokens, 100);
|
|
69
|
+
assert.strictEqual(s.sessionUsage.lastInputTokens, 250);
|
|
70
|
+
assert.strictEqual(s.sessionUsage.liveContextTokens, 1250);
|
|
71
|
+
|
|
72
|
+
assert.strictEqual(shouldAutoCompact(state({
|
|
73
|
+
sessionUsage: { liveContextTokens: 99, lastInputTokens: 99 },
|
|
74
|
+
})), false);
|
|
75
|
+
assert.strictEqual(shouldAutoCompact(state({
|
|
76
|
+
sessionUsage: { liveContextTokens: 150, lastInputTokens: 150 },
|
|
77
|
+
lastCompactedAt: Date.now(),
|
|
78
|
+
})), false);
|
|
79
|
+
assert.strictEqual(shouldAutoCompact(state({
|
|
80
|
+
sessionUsage: { liveContextTokens: 250, lastInputTokens: 250 },
|
|
81
|
+
lastCompactedAt: Date.now(),
|
|
82
|
+
})), true);
|
|
83
|
+
|
|
84
|
+
console.log("usage accounting OK");
|
package/web.js
CHANGED
|
@@ -70,7 +70,14 @@ function notifyAgentSpacePasswordChanged() {
|
|
|
70
70
|
const token = process.env.AGENTSPACE_POD_TOKEN;
|
|
71
71
|
if (!apiUrl || !token) return resolve({ ok: false, reason: "missing-env" });
|
|
72
72
|
let u;
|
|
73
|
-
try {
|
|
73
|
+
try {
|
|
74
|
+
const base = new URL(apiUrl);
|
|
75
|
+
if (base.pathname === "/") base.pathname = "/api/";
|
|
76
|
+
if (!base.pathname.endsWith("/")) base.pathname += "/";
|
|
77
|
+
u = new URL("pods/self/password-changed", base);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return resolve({ ok: false, reason: "bad-url" });
|
|
80
|
+
}
|
|
74
81
|
const lib = u.protocol === "https:" ? require("https") : require("http");
|
|
75
82
|
const req = lib.request({
|
|
76
83
|
method: "POST",
|
|
@@ -935,7 +942,11 @@ init();
|
|
|
935
942
|
|
|
936
943
|
// ── Server ─────────────────────────────────────────────────────────
|
|
937
944
|
|
|
945
|
+
let webServer = null;
|
|
946
|
+
|
|
938
947
|
function startWebServer() {
|
|
948
|
+
if (webServer) return webServer;
|
|
949
|
+
|
|
939
950
|
const server = http.createServer(async (req, res) => {
|
|
940
951
|
// CORS for local dev
|
|
941
952
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -977,6 +988,7 @@ function startWebServer() {
|
|
|
977
988
|
reconcileGrandfatheredPasswordChange();
|
|
978
989
|
});
|
|
979
990
|
|
|
991
|
+
webServer = server;
|
|
980
992
|
return server;
|
|
981
993
|
}
|
|
982
994
|
|