@linzumi/cli 1.0.16 → 1.0.18

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": "@linzumi/cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ // Enter->typing latency analyzer for a runner log (the measurement infra
3
+ // behind the 2026-07-02 follow-up-latency RCA; see
4
+ // test/enterToTypingHeadOfLine.test.ts for the fix it motivated).
5
+ //
6
+ // For every real-thread follow-up dispatch it pairs `kandan.message_queued`
7
+ // with the next `codex.turn_starting` on the same thread and classifies the
8
+ // window:
9
+ // - silent : NO other runner activity for the thread in between.
10
+ // Pre-fix this was the head-of-line block on the
11
+ // `queued` message_state server ack; post-fix it
12
+ // should be ~0ms.
13
+ // - drain_blocked/* : a logged `kandan.queue_drain_blocked` gate
14
+ // (turn_not_idle = user sent mid-turn, etc.). These
15
+ // are legitimate waits, not latency bugs.
16
+ // - cold_spawn : a `runner.thread_process_starting` fired in the
17
+ // window (worker cold respawn).
18
+ //
19
+ // Usage:
20
+ // node scripts/analyze_enter_to_typing_latency.mjs [path-to-runner-log]
21
+ // Default log path: ~/.linzumi/logs/linzumi-runner.log
22
+ //
23
+ // Ran against a production runner log on 2026-07-02 this reported, over
24
+ // 5439 follow-up dispatches (UUID-threaded; includes local vitest threads
25
+ // with synthetic UUIDs, which are all in the fast bucket): 204 slower than
26
+ // 500ms, of which 158 were `silent` (p50 1343ms / p90 3119ms / p99 11928ms),
27
+ // 46 were drain_blocked/turn_not_idle, and 0 were cold spawns - the evidence
28
+ // that the dominant Enter->typing latency was the awaited status-paint ack,
29
+ // not worker respawn.
30
+ import { createReadStream } from 'node:fs';
31
+ import { createInterface } from 'node:readline';
32
+ import { homedir } from 'node:os';
33
+ import path from 'node:path';
34
+
35
+ const logPath =
36
+ process.argv[2] ??
37
+ path.join(homedir(), '.linzumi', 'logs', 'linzumi-runner.log');
38
+ const slowThresholdMs = Number(process.env.SLOW_THRESHOLD_MS ?? 500);
39
+
40
+ // Real Linzumi thread ids are UUIDs; test threads use readable slugs.
41
+ const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
42
+
43
+ function percentile(sorted, q) {
44
+ if (sorted.length === 0) {
45
+ return null;
46
+ }
47
+ return sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))];
48
+ }
49
+
50
+ const threads = new Map();
51
+ const rows = [];
52
+
53
+ const rl = createInterface({
54
+ input: createReadStream(logPath),
55
+ crlfDelay: Infinity,
56
+ });
57
+
58
+ for await (const line of rl) {
59
+ if (line.length > 20_000) {
60
+ continue;
61
+ }
62
+ let entry;
63
+ try {
64
+ entry = JSON.parse(line);
65
+ } catch {
66
+ continue;
67
+ }
68
+ const threadId =
69
+ entry.thread_id ??
70
+ entry.kandanThreadId ??
71
+ entry.linzumi_thread_id ??
72
+ entry.threadId ??
73
+ entry.kandan_thread_id;
74
+ if (typeof threadId !== 'string' || !uuidRe.test(threadId)) {
75
+ continue;
76
+ }
77
+ const atMs = Date.parse(entry.ts);
78
+ if (Number.isNaN(atMs)) {
79
+ // A malformed/absent timestamp would otherwise mint a NaN-latency row
80
+ // that silently falls out of both the slow and fast buckets, making the
81
+ // printed totals diverge from the dispatch count.
82
+ continue;
83
+ }
84
+ let state = threads.get(threadId);
85
+ if (state === undefined) {
86
+ state = { pending: null };
87
+ threads.set(threadId, state);
88
+ }
89
+ const event = entry.event;
90
+ if (event === 'kandan.message_queued') {
91
+ state.pending = { atMs, seq: entry.seq, blocked: [], spawn: false };
92
+ } else if (state.pending !== null) {
93
+ if (event === 'codex.turn_starting') {
94
+ rows.push({
95
+ threadId,
96
+ ts: entry.ts,
97
+ ms: atMs - state.pending.atMs,
98
+ blocked: state.pending.blocked,
99
+ spawn: state.pending.spawn,
100
+ });
101
+ state.pending = null;
102
+ } else if (event === 'kandan.queue_drain_blocked') {
103
+ state.pending.blocked.push(entry.reason);
104
+ } else if (event === 'runner.thread_process_starting') {
105
+ state.pending.spawn = true;
106
+ }
107
+ }
108
+ }
109
+
110
+ const slow = rows.filter((row) => row.ms > slowThresholdMs);
111
+ const silent = slow.filter((row) => row.blocked.length === 0 && !row.spawn);
112
+ const spawns = slow.filter((row) => row.spawn);
113
+ const blockedCounts = new Map();
114
+ for (const row of slow) {
115
+ for (const reason of new Set(row.blocked)) {
116
+ blockedCounts.set(reason, (blockedCounts.get(reason) ?? 0) + 1);
117
+ }
118
+ }
119
+
120
+ const silentSorted = silent.map((row) => row.ms).sort((a, b) => a - b);
121
+ const fast = rows.filter((row) => row.ms <= slowThresholdMs);
122
+ const fastSorted = fast.map((row) => row.ms).sort((a, b) => a - b);
123
+
124
+ console.log(`log: ${logPath}`);
125
+ console.log(
126
+ `follow-up dispatches=${rows.length} slow(>${slowThresholdMs}ms)=${slow.length}`
127
+ );
128
+ console.log(
129
+ ` silent (head-of-line on paint ack pre-fix)=${silent.length}` +
130
+ (silentSorted.length > 0
131
+ ? ` p50=${percentile(silentSorted, 0.5)}ms p90=${percentile(silentSorted, 0.9)}ms p99=${percentile(silentSorted, 0.99)}ms max=${silentSorted[silentSorted.length - 1]}ms`
132
+ : '')
133
+ );
134
+ for (const [reason, count] of blockedCounts) {
135
+ console.log(` drain_blocked/${reason}=${count}`);
136
+ }
137
+ console.log(` cold_spawn=${spawns.length}`);
138
+ console.log(
139
+ `fast(<=${slowThresholdMs}ms)=${fast.length}` +
140
+ (fastSorted.length > 0 ? ` p50=${percentile(fastSorted, 0.5)}ms` : '')
141
+ );
142
+ if (silent.length > 0) {
143
+ console.log('\nslowest silent windows:');
144
+ for (const row of [...silent].sort((a, b) => b.ms - a.ms).slice(0, 10)) {
145
+ console.log(` ${row.ts} thread=${row.threadId.slice(0, 8)} ${row.ms}ms`);
146
+ }
147
+ }