@saccolabs/pi-claude-cli 0.4.8 → 0.4.10
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/index.ts +10 -12
- package/package.json +1 -1
- package/src/event-bridge.ts +37 -4
- package/src/rate-limit.ts +64 -0
package/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./src/process-manager.js";
|
|
17
17
|
import { getCustomToolDefs, writeMcpConfig } from "./src/mcp-config.js";
|
|
18
18
|
import { rewriteOverflowMessage } from "./src/overflow.js";
|
|
19
|
+
import { buildRateLimitPayload, rateLimitIdentity } from "./src/rate-limit.js";
|
|
19
20
|
|
|
20
21
|
// Kill all active Claude subprocesses on process exit to prevent orphans
|
|
21
22
|
process.on("exit", killAllProcesses);
|
|
@@ -40,21 +41,18 @@ let lastRateLimitJson: string | undefined;
|
|
|
40
41
|
function publishRateLimit(info: Record<string, unknown>): void {
|
|
41
42
|
const setStatus = uiContext?.ui?.setStatus;
|
|
42
43
|
if (typeof setStatus !== "function") return;
|
|
43
|
-
const payload =
|
|
44
|
-
status: info.status,
|
|
45
|
-
resetsAt: info.resetsAt,
|
|
46
|
-
rateLimitType: info.rateLimitType,
|
|
47
|
-
overageStatus: info.overageStatus,
|
|
48
|
-
isUsingOverage: info.isUsingOverage === true,
|
|
49
|
-
observedAt: Math.floor(Date.now() / 1000),
|
|
50
|
-
});
|
|
44
|
+
const payload = buildRateLimitPayload(info);
|
|
51
45
|
// Push only on change: the event repeats every turn, and a status that
|
|
52
46
|
// rewrites itself constantly is noise for whatever renders it.
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
lastRateLimitJson =
|
|
47
|
+
const identity = rateLimitIdentity(payload);
|
|
48
|
+
if (identity === lastRateLimitJson) return;
|
|
49
|
+
lastRateLimitJson = identity;
|
|
56
50
|
try {
|
|
57
|
-
setStatus.call(
|
|
51
|
+
setStatus.call(
|
|
52
|
+
uiContext!.ui,
|
|
53
|
+
RATE_LIMIT_STATUS_KEY,
|
|
54
|
+
JSON.stringify(payload),
|
|
55
|
+
);
|
|
58
56
|
} catch {
|
|
59
57
|
/* never break a turn over a status push */
|
|
60
58
|
}
|
package/package.json
CHANGED
package/src/event-bridge.ts
CHANGED
|
@@ -131,9 +131,35 @@ export function createEventBridge(
|
|
|
131
131
|
cache_creation_input_tokens: 0,
|
|
132
132
|
};
|
|
133
133
|
let cycleUsage: ClaudeUsage = {};
|
|
134
|
+
/**
|
|
135
|
+
* Context size of the most recent cycle — what the model actually saw on
|
|
136
|
+
* its last call, which is NOT the episode's summed usage.
|
|
137
|
+
*
|
|
138
|
+
* pi reads `usage.totalTokens` as the conversation's context size
|
|
139
|
+
* (`calculateContextTokens` in pi's compaction module short-circuits on it)
|
|
140
|
+
* and uses it for both the context gauge and the auto-compaction trigger.
|
|
141
|
+
* Every cycle re-sends the same cached prefix, so summing cycles counts
|
|
142
|
+
* that prefix once per cycle: a captured 3-cycle episode sums to 82,174
|
|
143
|
+
* while the model's real context never exceeded 28,243. The host then
|
|
144
|
+
* showed 41% of a 200k window instead of 14%, and a long enough turn
|
|
145
|
+
* crossed pi's compaction threshold at a fraction of true occupancy.
|
|
146
|
+
*
|
|
147
|
+
* So the four component figures stay cumulative — they are what gets
|
|
148
|
+
* billed — and `totalTokens` carries the last cycle's context instead.
|
|
149
|
+
*/
|
|
150
|
+
let lastCycleContext = 0;
|
|
134
151
|
/** Tool ids already surfaced as markers (envelope arrives once per block). */
|
|
135
152
|
const markedToolIds = new Set<string>();
|
|
136
153
|
|
|
154
|
+
/** Prompt size of one cycle: everything the model read, excluding output. */
|
|
155
|
+
function contextOf(usage: ClaudeUsage): number {
|
|
156
|
+
return (
|
|
157
|
+
(usage.input_tokens ?? 0) +
|
|
158
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
159
|
+
(usage.cache_creation_input_tokens ?? 0)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
function recomputeUsage(): void {
|
|
138
164
|
output.usage.input =
|
|
139
165
|
cumulativeUsage.input_tokens + (cycleUsage.input_tokens ?? 0);
|
|
@@ -145,11 +171,18 @@ export function createEventBridge(
|
|
|
145
171
|
output.usage.cacheWrite =
|
|
146
172
|
cumulativeUsage.cache_creation_input_tokens +
|
|
147
173
|
(cycleUsage.cache_creation_input_tokens ?? 0);
|
|
174
|
+
// Latch the newest cycle we have numbers for. `applyResult` clears
|
|
175
|
+
// cycleUsage, so a zero here means "no fresher figure", never "empty
|
|
176
|
+
// context" — the last latched value must survive.
|
|
177
|
+
const cycleContext = contextOf(cycleUsage);
|
|
178
|
+
if (cycleContext > 0) lastCycleContext = cycleContext;
|
|
148
179
|
output.usage.totalTokens =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
180
|
+
lastCycleContext > 0
|
|
181
|
+
? lastCycleContext
|
|
182
|
+
: output.usage.input +
|
|
183
|
+
output.usage.output +
|
|
184
|
+
output.usage.cacheRead +
|
|
185
|
+
output.usage.cacheWrite;
|
|
153
186
|
calculateCost(model, output.usage);
|
|
154
187
|
}
|
|
155
188
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account rate-limit state, shaped for the host's status channel.
|
|
3
|
+
*
|
|
4
|
+
* The CLI emits a `rate_limit_event` per turn describing ONE window: the first
|
|
5
|
+
* one whose warning threshold has been crossed, walking
|
|
6
|
+
* `5h -> 7d -> 7d_oi -> overage`. So the reported window is the binding
|
|
7
|
+
* constraint, not an arbitrary pick — and there is no way to see all four at
|
|
8
|
+
* once from this stream. A front-end that wants "which limit will stop me, how
|
|
9
|
+
* close am I, and when does it reset" has everything it needs; one that wants
|
|
10
|
+
* a full dashboard does not, and should not pretend otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Kept pure and separate from `index.ts` so the payload contract is testable
|
|
13
|
+
* without a pi runtime.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** What the host receives under the `claude-rate-limit` status key. */
|
|
17
|
+
export interface RateLimitPayload {
|
|
18
|
+
status: unknown;
|
|
19
|
+
resetsAt: unknown;
|
|
20
|
+
rateLimitType: unknown;
|
|
21
|
+
overageStatus: unknown;
|
|
22
|
+
isUsingOverage: boolean;
|
|
23
|
+
/** Fraction of the window consumed: 1.01 means 101%, i.e. over. */
|
|
24
|
+
utilization: number | null;
|
|
25
|
+
/** Which warning step tripped, when one has. */
|
|
26
|
+
surpassedThreshold: number | null;
|
|
27
|
+
observedAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function num(value: unknown): number | null {
|
|
31
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildRateLimitPayload(
|
|
35
|
+
info: Record<string, unknown>,
|
|
36
|
+
nowSeconds: number = Math.floor(Date.now() / 1000),
|
|
37
|
+
): RateLimitPayload {
|
|
38
|
+
return {
|
|
39
|
+
status: info.status,
|
|
40
|
+
resetsAt: info.resetsAt,
|
|
41
|
+
rateLimitType: info.rateLimitType,
|
|
42
|
+
overageStatus: info.overageStatus,
|
|
43
|
+
isUsingOverage: info.isUsingOverage === true,
|
|
44
|
+
// The CLI has always sent these two; dropping them left front-ends able to
|
|
45
|
+
// say WHICH limit was in play and when it resets, but never how close it
|
|
46
|
+
// was — so a user could not watch themselves approach a wall, only hit it.
|
|
47
|
+
utilization: num(info.utilization),
|
|
48
|
+
surpassedThreshold: num(info.surpassedThreshold),
|
|
49
|
+
observedAt: nowSeconds,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Identity of a payload ignoring `observedAt`, for change detection.
|
|
55
|
+
*
|
|
56
|
+
* The event repeats every turn with a fresh timestamp; pushing that verbatim
|
|
57
|
+
* would rewrite the host's status constantly and make anything rendering it
|
|
58
|
+
* flicker. Comparing everything EXCEPT the timestamp is what makes the push
|
|
59
|
+
* "on change" rather than "on turn".
|
|
60
|
+
*/
|
|
61
|
+
export function rateLimitIdentity(payload: RateLimitPayload): string {
|
|
62
|
+
const { observedAt: _observedAt, ...rest } = payload;
|
|
63
|
+
return JSON.stringify(rest);
|
|
64
|
+
}
|