claude-code-runrate 0.2.3 → 0.3.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 +70 -3
- package/bin/ccr.js +16 -0
- package/package.json +1 -1
- package/scripts/launch.sh +100 -11
- package/sidecar/ccr.tmux.conf +12 -4
- package/src/burn.js +7 -3
- package/src/cycle-view.js +78 -0
- package/src/doctor.js +2 -0
- package/src/economy-model.js +3 -0
- package/src/launch-vscode.js +44 -11
- package/src/liveness.js +18 -1
- package/src/normalize.js +24 -5
- package/src/pane-blob.js +249 -0
- package/src/pane-config.js +109 -0
- package/src/rate-limits.js +12 -2
- package/src/render/economy.js +10 -2
- package/src/render/pane.js +186 -0
- package/src/render/shared.js +66 -10
- package/src/render/statusline.js +3 -0
- package/src/safe-read.js +66 -0
- package/src/sanitize.js +45 -4
- package/src/sidecar.js +238 -17
- package/src/transcripts.js +31 -15
package/src/transcripts.js
CHANGED
|
@@ -87,7 +87,11 @@ function parseEvents(input) {
|
|
|
87
87
|
const meta = { sessionId: /** @type {string|null} */(null), cwd: /** @type {string|null} */(null), gitBranch: /** @type {string|null} */(null), version: /** @type {string|null} */(null), startTs: /** @type {number|null} */(null), lastTs: /** @type {number|null} */(null) };
|
|
88
88
|
/** @type {{ ts: number|null, kind: 'tool'|'cmd', tool: string, arg: string }[]} */
|
|
89
89
|
const events = [];
|
|
90
|
-
|
|
90
|
+
// Null-prototype: keys are tool NAMES straight out of the transcript. On a
|
|
91
|
+
// plain object, `tools['constructor'] || 0` reads the inherited Object
|
|
92
|
+
// constructor (which then rendered as its native source in the feed header),
|
|
93
|
+
// and `tools['__proto__'] = n` performs a prototype write instead of counting.
|
|
94
|
+
const tools = /** @type {Record<string, number>} */ (Object.create(null));
|
|
91
95
|
const tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
92
96
|
const files = new Set();
|
|
93
97
|
const models = new Set();
|
|
@@ -111,7 +115,7 @@ function parseEvents(input) {
|
|
|
111
115
|
if (meta.sessionId == null && o.sessionId) meta.sessionId = stripControl(String(o.sessionId));
|
|
112
116
|
if (meta.cwd == null && o.cwd) meta.cwd = stripControl(String(o.cwd));
|
|
113
117
|
if (meta.gitBranch == null && o.gitBranch) meta.gitBranch = stripControl(String(o.gitBranch));
|
|
114
|
-
if (meta.version == null && o.version) meta.version = o.version;
|
|
118
|
+
if (meta.version == null && o.version) meta.version = stripControl(o.version);
|
|
115
119
|
const ts = o.timestamp ? Date.parse(o.timestamp) : NaN;
|
|
116
120
|
const tsOk = Number.isFinite(ts) ? ts : null;
|
|
117
121
|
if (tsOk != null) {
|
|
@@ -137,7 +141,10 @@ function parseEvents(input) {
|
|
|
137
141
|
if (o.type === 'assistant') {
|
|
138
142
|
assistantTurns++;
|
|
139
143
|
const msg = o.message || {};
|
|
140
|
-
|
|
144
|
+
// Sanitize here, not at the renderer: parseEvents is the documented choke
|
|
145
|
+
// point "so every renderer is covered", and lastModel/models are consumed
|
|
146
|
+
// by src/render/resume.js outside the sidecar's graph.
|
|
147
|
+
if (msg.model) { const m = stripControl(msg.model); models.add(m); lastModel = m; }
|
|
141
148
|
const u = msg.usage;
|
|
142
149
|
if (u) {
|
|
143
150
|
const turn = { input: u.input_tokens || 0, output: u.output_tokens || 0, cacheRead: u.cache_read_input_tokens || 0, cacheCreate: u.cache_creation_input_tokens || 0 };
|
|
@@ -223,36 +230,45 @@ const MAX_READ = 4 * 1024 * 1024; // bound one tick's allocation; large backlogs
|
|
|
223
230
|
* transcript is append-only; if it shrank (rotation/truncation) we restart at 0.
|
|
224
231
|
* Reads at most `maxRead` bytes per call (bounded allocation), so a very large
|
|
225
232
|
* transcript is consumed over several ticks rather than in one giant buffer.
|
|
226
|
-
* Returns the new byte offset (advanced only past whole lines)
|
|
233
|
+
* Returns the new byte offset (advanced only past whole lines), and `restarted`
|
|
234
|
+
* — true when the file had shrunk and the tail went back to 0. Callers that
|
|
235
|
+
* ACCUMULATE across calls must reset their totals when they see it, or they
|
|
236
|
+
* re-add everything the restart is about to replay.
|
|
227
237
|
* @param {string} file
|
|
228
238
|
* @param {number} [fromOffset]
|
|
229
239
|
* @param {number} [maxRead]
|
|
230
|
-
* @returns {{ offset: number, lines: string[] }}
|
|
240
|
+
* @returns {{ offset: number, lines: string[], restarted: boolean }}
|
|
231
241
|
*/
|
|
232
242
|
function readNewLines(file, fromOffset = 0, maxRead = MAX_READ) {
|
|
233
|
-
let st; try { st = fs.statSync(file); } catch { return { offset: fromOffset, lines: [] }; }
|
|
243
|
+
let st; try { st = fs.statSync(file); } catch { return { offset: fromOffset, lines: [], restarted: false }; }
|
|
234
244
|
let start = fromOffset;
|
|
235
|
-
|
|
245
|
+
const restarted = st.size < start; // truncated/rotated → restart
|
|
246
|
+
if (restarted) start = 0;
|
|
236
247
|
let len = st.size - start;
|
|
237
|
-
if (len <= 0) return { offset: st.size, lines: [] };
|
|
248
|
+
if (len <= 0) return { offset: st.size, lines: [], restarted };
|
|
238
249
|
const capped = len > maxRead; // more data than one window holds
|
|
239
250
|
if (capped) len = maxRead;
|
|
240
251
|
const fd = fs.openSync(file, 'r');
|
|
241
252
|
try {
|
|
242
253
|
const buf = Buffer.alloc(len);
|
|
243
|
-
fs.readSync(fd, buf, 0, len, start);
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
const read = fs.readSync(fd, buf, 0, len, start);
|
|
255
|
+
// Find the line break in the BYTES, and advance by bytes. Decoding first and
|
|
256
|
+
// re-encoding the kept prefix (what this used to do) is not a round trip:
|
|
257
|
+
// every invalid UTF-8 byte decodes to U+FFFD and re-encodes to THREE bytes,
|
|
258
|
+
// so the offset overshot the file — after which `st.size < start` reads as a
|
|
259
|
+
// truncation, the tail restarts at 0, and the whole transcript is re-ingested
|
|
260
|
+
// every tick, forever, with the stats inflating each time. One stray binary
|
|
261
|
+
// byte in a tool result was enough. Byte arithmetic has no such failure.
|
|
262
|
+
const lastNl = buf.lastIndexOf(0x0a, read - 1);
|
|
246
263
|
if (lastNl < 0) {
|
|
247
264
|
// No complete line in this window. If capped, the current line is longer
|
|
248
265
|
// than the cap — skip past the window to guarantee forward progress (the
|
|
249
266
|
// resulting partial line fails JSON.parse and is tolerated). Otherwise the
|
|
250
267
|
// last line just isn't finished yet; wait for more.
|
|
251
|
-
return capped ? { offset: start + len, lines: [] } : { offset: start, lines: [] };
|
|
268
|
+
return capped ? { offset: start + len, lines: [], restarted } : { offset: start, lines: [], restarted };
|
|
252
269
|
}
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
return { offset: consumed, lines: whole.split('\n').filter(Boolean) };
|
|
270
|
+
const text = buf.subarray(0, lastNl).toString('utf8');
|
|
271
|
+
return { offset: start + lastNl + 1, lines: text.split('\n').filter(Boolean), restarted };
|
|
256
272
|
} finally {
|
|
257
273
|
fs.closeSync(fd);
|
|
258
274
|
}
|