claude-rpc 0.13.2 → 0.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/community.js +22 -31
- package/src/daemon.js +2 -2
- package/src/version.js +1 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1179,6 +1179,12 @@ async function profileVerify() {
|
|
|
1179
1179
|
};
|
|
1180
1180
|
|
|
1181
1181
|
try {
|
|
1182
|
+
// Make sure the profile row exists server-side before we verify it, so
|
|
1183
|
+
// verification works regardless of whether `profile publish` was run first.
|
|
1184
|
+
if (lb.profileIsPublishable(profile)) {
|
|
1185
|
+
const { flushProfile } = await import('./community.js');
|
|
1186
|
+
await flushProfile(cfg);
|
|
1187
|
+
}
|
|
1182
1188
|
console.log(`${c.dim}requesting a verification token…${c.reset}`);
|
|
1183
1189
|
const start = await post('/verify/start', { instanceId: community.instanceId, githubUser: profile.githubUser });
|
|
1184
1190
|
if (!start.json?.token) return fail(`verify/start failed: ${start.json?.error || start.status}`, { code: EX_SYS_ERROR });
|
package/src/community.js
CHANGED
|
@@ -26,7 +26,6 @@ import { VERSION } from './version.js';
|
|
|
26
26
|
import { profileIsPublishable } from './leaderboard.js';
|
|
27
27
|
|
|
28
28
|
const CURSOR_PATH = join(STATE_DIR, 'community-cursor.json');
|
|
29
|
-
const PROFILE_CURSOR_PATH = join(STATE_DIR, 'profile-cursor.json');
|
|
30
29
|
|
|
31
30
|
export function readCursor(path = CURSOR_PATH) {
|
|
32
31
|
if (!existsSync(path)) return { sessions: 0, tokens: 0, ts: 0 };
|
|
@@ -54,6 +53,15 @@ export function osFamily() {
|
|
|
54
53
|
return 'linux';
|
|
55
54
|
}
|
|
56
55
|
|
|
56
|
+
// Per-report caps. These mirror the worker's validateReport limits — the
|
|
57
|
+
// client CLAMPS each delta to them so a large first-time backfill (a heavy
|
|
58
|
+
// user's whole lifetime total on the very first report) STREAMS over multiple
|
|
59
|
+
// flushes instead of being rejected. Without this, anyone with >5B lifetime
|
|
60
|
+
// tokens would 400 forever (the cursor never advances on a rejected report) and
|
|
61
|
+
// be silently dropped from the community totals.
|
|
62
|
+
const MAX_REPORT_SESSIONS = 100_000;
|
|
63
|
+
const MAX_REPORT_TOKENS = 5_000_000_000;
|
|
64
|
+
|
|
57
65
|
// Pure: given an aggregate and a cursor, produce the next payload. The
|
|
58
66
|
// worker's validateReport must accept this shape; if you add a field
|
|
59
67
|
// here, add it there too.
|
|
@@ -65,8 +73,8 @@ export function buildPayload(aggregate, cursor, { instanceId, now = Date.now() }
|
|
|
65
73
|
+ (aggregate?.cacheWriteTokens || 0);
|
|
66
74
|
return {
|
|
67
75
|
instanceId,
|
|
68
|
-
sessionsDelta: Math.max(0, sessions - (cursor.sessions || 0)),
|
|
69
|
-
tokensDelta: Math.max(0, tokens - (cursor.tokens || 0)),
|
|
76
|
+
sessionsDelta: Math.min(MAX_REPORT_SESSIONS, Math.max(0, sessions - (cursor.sessions || 0))),
|
|
77
|
+
tokensDelta: Math.min(MAX_REPORT_TOKENS, Math.max(0, tokens - (cursor.tokens || 0))),
|
|
70
78
|
version: VERSION,
|
|
71
79
|
osFamily: osFamily(),
|
|
72
80
|
ts: now,
|
|
@@ -87,25 +95,20 @@ function totalTokens(aggregate) {
|
|
|
87
95
|
+ (aggregate?.cacheWriteTokens || 0);
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function buildProfilePayload(aggregate, profileCfg, cursor, { instanceId, now = Date.now() }) {
|
|
98
|
-
const sessions = aggregate?.sessions || 0;
|
|
99
|
-
const tokens = totalTokens(aggregate);
|
|
100
|
-
const activeMs = aggregate?.activeMs || 0;
|
|
98
|
+
// A profile reports ABSOLUTE lifetime totals (not deltas). It's per-user and
|
|
99
|
+
// keyed by the instanceId, so the server just stores the latest value — no
|
|
100
|
+
// cursor, no double-count risk, and the board matches your real aggregate
|
|
101
|
+
// exactly. (Deltas were wrong here: the first publish carried the entire
|
|
102
|
+
// lifetime total, which blew past the per-report caps for any established user.)
|
|
103
|
+
export function buildProfilePayload(aggregate, profileCfg, { instanceId, now = Date.now() }) {
|
|
101
104
|
return {
|
|
102
105
|
instanceId,
|
|
103
106
|
handle: profileCfg.handle,
|
|
104
107
|
displayName: profileCfg.displayName || null,
|
|
105
108
|
githubUser: profileCfg.githubUser || null,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
tokens: totalTokens(aggregate),
|
|
110
|
+
sessions: aggregate?.sessions || 0,
|
|
111
|
+
activeMs: aggregate?.activeMs || 0,
|
|
109
112
|
streak: aggregate?.streak || 0,
|
|
110
113
|
version: VERSION,
|
|
111
114
|
osFamily: osFamily(),
|
|
@@ -115,7 +118,6 @@ export function buildProfilePayload(aggregate, profileCfg, cursor, { instanceId,
|
|
|
115
118
|
|
|
116
119
|
export async function flushProfile(cfg, {
|
|
117
120
|
aggregatePath = AGGREGATE_PATH,
|
|
118
|
-
cursorPath = PROFILE_CURSOR_PATH,
|
|
119
121
|
fetchImpl = globalThis.fetch,
|
|
120
122
|
} = {}) {
|
|
121
123
|
const profile = cfg?.profile || {};
|
|
@@ -130,9 +132,7 @@ export async function flushProfile(cfg, {
|
|
|
130
132
|
try { aggregate = JSON.parse(readFileSync(aggregatePath, 'utf8')); }
|
|
131
133
|
catch { return { ok: false, reason: 'unreadable-aggregate' }; }
|
|
132
134
|
|
|
133
|
-
const
|
|
134
|
-
const payload = buildProfilePayload(aggregate, profile, cursor, { instanceId });
|
|
135
|
-
|
|
135
|
+
const payload = buildProfilePayload(aggregate, profile, { instanceId });
|
|
136
136
|
const url = community.endpoint.replace(/\/+$/, '') + '/profile';
|
|
137
137
|
let res;
|
|
138
138
|
try {
|
|
@@ -148,16 +148,7 @@ export async function flushProfile(cfg, {
|
|
|
148
148
|
if (res.status === 429) return { ok: false, reason: 'rate-limited' };
|
|
149
149
|
return { ok: false, reason: `http-${res.status}` };
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
// Advance the cursor only on acceptance (same reasoning as flushCommunity).
|
|
153
|
-
writeCursor({
|
|
154
|
-
sessions: (cursor.sessions || 0) + payload.sessionsDelta,
|
|
155
|
-
tokens: (cursor.tokens || 0) + payload.tokensDelta,
|
|
156
|
-
activeMs: (cursor.activeMs || 0) + payload.activeMsDelta,
|
|
157
|
-
ts: payload.ts,
|
|
158
|
-
}, cursorPath);
|
|
159
|
-
|
|
160
|
-
return { ok: true, delta: { sessions: payload.sessionsDelta, tokens: payload.tokensDelta, activeMs: payload.activeMsDelta } };
|
|
151
|
+
return { ok: true, totals: { tokens: payload.tokens, sessions: payload.sessions, activeMs: payload.activeMs } };
|
|
161
152
|
}
|
|
162
153
|
|
|
163
154
|
// Single best-effort flush. Returns { ok, reason, delta? } — never throws.
|
package/src/daemon.js
CHANGED
|
@@ -627,8 +627,8 @@ async function runCommunityFlush() {
|
|
|
627
627
|
}
|
|
628
628
|
if (config.profile?.enabled) {
|
|
629
629
|
const pr = await flushProfile(config);
|
|
630
|
-
if (pr.ok && pr.
|
|
631
|
-
log(`profile: published @${config.profile.handle} (
|
|
630
|
+
if (pr.ok && pr.totals) {
|
|
631
|
+
log(`profile: published @${config.profile.handle} (${pr.totals.tokens} tokens)`);
|
|
632
632
|
} else if (!pr.ok && pr.reason !== 'rate-limited' && pr.reason !== 'disabled') {
|
|
633
633
|
log(`profile: ${pr.reason}${pr.error ? ' (' + pr.error + ')' : ''}`);
|
|
634
634
|
}
|