@jameslovespancakes/pi-plus 1.0.7 → 1.0.9
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jameslovespancakes/pi-plus",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Consolidated pi extensions: Claude subscription accounts and quota HUD, benchmark-driven model catalog and billing policy, live agent board, and capacity-gated remote test workers.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -163,7 +163,17 @@ export function applyQuotaHeaders(
|
|
|
163
163
|
const account = storage?.accounts.find((a) => a.id === accountId);
|
|
164
164
|
if (!account) return false;
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// Headers arrive on every response, but utilisation moves in whole percent
|
|
167
|
+
// steps over windows of hours. Rewriting two credential files per request to
|
|
168
|
+
// store an unchanged number is pure write amplification, and on Windows each
|
|
169
|
+
// rewrite is another chance for the rename to collide with a file lock.
|
|
170
|
+
const previous = account.quota;
|
|
171
|
+
const unchanged =
|
|
172
|
+
previous?.five_hour?.usedPercent === fresh.five_hour?.usedPercent
|
|
173
|
+
&& previous?.seven_day?.usedPercent === fresh.seven_day?.usedPercent;
|
|
174
|
+
if (unchanged) return false;
|
|
175
|
+
|
|
176
|
+
saveAccount({ ...account, quota: { ...fresh, scoped: previous?.scoped } });
|
|
167
177
|
return true;
|
|
168
178
|
}
|
|
169
179
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
|
|
@@ -93,14 +93,45 @@ function readJson<T>(path: string): T | undefined {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Temp-file + rename, so a crash cannot leave a half-written credential file.
|
|
98
|
+
*
|
|
99
|
+
* The rename is retried because on Windows it fails with EPERM whenever the
|
|
100
|
+
* destination is momentarily held open by someone else: a virus scanner
|
|
101
|
+
* examining the file we just wrote, the search indexer, or another pi session
|
|
102
|
+
* reading it. The temp name carries the pid so concurrent writers never share
|
|
103
|
+
* a temp file, but they can still collide on the destination.
|
|
104
|
+
*
|
|
105
|
+
* Retries are brief and synchronous. Losing a quota update is harmless, so a
|
|
106
|
+
* final failure removes the temp file and gives up rather than throwing into
|
|
107
|
+
* a live request.
|
|
108
|
+
*/
|
|
97
109
|
function writeJson(path: string, value: unknown): void {
|
|
98
110
|
mkdirSync(dirname(path), { recursive: true });
|
|
99
111
|
const temp = `${path}.${process.pid}.tmp`;
|
|
100
112
|
writeFileSync(temp, JSON.stringify(value, undefined, 2), { encoding: "utf8", mode: 0o600 });
|
|
101
|
-
|
|
113
|
+
|
|
114
|
+
const TRANSIENT = new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY"]);
|
|
115
|
+
for (let attempt = 0; ; attempt++) {
|
|
116
|
+
try {
|
|
117
|
+
renameSync(temp, path);
|
|
118
|
+
return;
|
|
119
|
+
} catch (error: any) {
|
|
120
|
+
if (attempt >= 5 || !TRANSIENT.has(error?.code)) {
|
|
121
|
+
try { rmSync(temp, { force: true }); } catch { /* best effort */ }
|
|
122
|
+
if (attempt >= 5) return; // transient and unresolved: drop this write
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
// 1, 2, 4, 8, 16ms. Long enough for a scanner to release the handle,
|
|
126
|
+
// short enough not to stall the request that triggered it.
|
|
127
|
+
Atomics.wait(SPIN, 0, 0, 2 ** attempt);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
102
130
|
}
|
|
103
131
|
|
|
132
|
+
/** Backing store for the synchronous sleep above. */
|
|
133
|
+
const SPIN = new Int32Array(new SharedArrayBuffer(4));
|
|
134
|
+
|
|
104
135
|
const pick = <T extends object>(source: any, keys: readonly string[]): T =>
|
|
105
136
|
Object.fromEntries(keys.filter((k) => source?.[k] !== undefined).map((k) => [k, source[k]])) as T;
|
|
106
137
|
|
|
@@ -73,9 +73,16 @@ export function routeAccessToken(primary: string, modelId?: string, _sessionId?:
|
|
|
73
73
|
return primary;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
lastSelected = { id: picked.candidate.id, at: now };
|
|
77
78
|
if (picked.candidate.account) {
|
|
78
|
-
|
|
79
|
+
// `lastUsed` only orders the footer's account list, so second-level
|
|
80
|
+
// precision buys nothing. Persisting it on every request rewrote two
|
|
81
|
+
// credential files per call; once a minute is indistinguishable in the UI.
|
|
82
|
+
const previous = picked.candidate.account.lastUsed ?? 0;
|
|
83
|
+
if (now - previous > 60_000) {
|
|
84
|
+
saveAccount({ ...picked.candidate.account, lastUsed: now });
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
return picked.candidate.access ?? primary;
|
|
81
88
|
}
|