glm-coding-router 1.1.1 → 2.0.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.
Files changed (39) hide show
  1. package/README.md +534 -419
  2. package/dist/bin/glm-review.js +46 -4
  3. package/dist/bin/glm-worker.js +37 -4
  4. package/dist/budget/estimator.js +218 -0
  5. package/dist/budget/manager.js +223 -0
  6. package/dist/cli.js +38 -0
  7. package/dist/commands/benchmark.js +4 -0
  8. package/dist/commands/dashboard.js +348 -0
  9. package/dist/commands/runs.js +568 -0
  10. package/dist/commands/usage.js +1 -40
  11. package/dist/commands/watch.js +289 -0
  12. package/dist/core/agent-args.js +20 -0
  13. package/dist/core/config.js +61 -0
  14. package/dist/core/errors.js +24 -0
  15. package/dist/core/paths.js +32 -0
  16. package/dist/core/process.js +83 -0
  17. package/dist/core/prompt.js +18 -5
  18. package/dist/core/routing-flags.js +59 -0
  19. package/dist/core/zai-quota.js +46 -0
  20. package/dist/events/bus.js +64 -0
  21. package/dist/events/claude-adapter.js +416 -0
  22. package/dist/events/types.js +9 -0
  23. package/dist/handoff/bundle.js +203 -0
  24. package/dist/handoff/parent-handoff.js +48 -0
  25. package/dist/mcp/server.js +45 -1
  26. package/dist/routing/glm-routing.js +131 -0
  27. package/dist/runs/checkpoint.js +204 -0
  28. package/dist/runs/drain.js +165 -0
  29. package/dist/runs/heartbeat.js +45 -0
  30. package/dist/runs/registry.js +350 -0
  31. package/dist/runs/store.js +186 -0
  32. package/dist/runs/ulid.js +112 -0
  33. package/dist/runs/worker-run.js +672 -0
  34. package/dist/templates/agents-block.js +9 -0
  35. package/dist/templates/claude-block.js +9 -0
  36. package/dist/templates/glm-delegation-skill.js +76 -65
  37. package/dist/tui/progress.js +338 -0
  38. package/dist/tui/render.js +78 -0
  39. package/package.json +1 -1
@@ -0,0 +1,186 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { logger } from "../core/logging.js";
4
+ /** The one file every run is guaranteed to leave behind, even when it crashed. */
5
+ export const EVENTS_FILE_NAME = "events.jsonl";
6
+ /** `events.jsonl` inside a run directory; exported so every module spells the path the same way. */
7
+ export function eventsFilePath(runDirPath) {
8
+ return path.join(runDirPath, EVENTS_FILE_NAME);
9
+ }
10
+ /**
11
+ * Opens `events.jsonl` in append mode. Append mode is the whole crash story:
12
+ * the file is never truncated, every line reaches the OS as it is appended,
13
+ * and a killed process leaves at worst one half-written final line — which
14
+ * `readEvents` tolerates — and never a summary, which is precisely how
15
+ * `runs show` tells a crashed run from a finished one.
16
+ *
17
+ * `append` may throw (disk full, fd gone); the event bus catches subscriber
18
+ * exceptions, so a broken store degrades to lost lines instead of a dead run.
19
+ */
20
+ export function openRunStore(runDirPath) {
21
+ fs.mkdirSync(runDirPath, { recursive: true });
22
+ const file = eventsFilePath(runDirPath);
23
+ const fd = fs.openSync(file, "a");
24
+ let closed = false;
25
+ return {
26
+ path: file,
27
+ append(event) {
28
+ if (closed) {
29
+ logger.debug(`run store: append after close on ${file} skipped`);
30
+ return;
31
+ }
32
+ fs.writeSync(fd, JSON.stringify(event) + "\n", null, "utf8");
33
+ },
34
+ flush() {
35
+ if (!closed) {
36
+ fs.fsyncSync(fd);
37
+ }
38
+ },
39
+ close() {
40
+ if (closed) {
41
+ return;
42
+ }
43
+ closed = true;
44
+ try {
45
+ fs.fsyncSync(fd);
46
+ }
47
+ finally {
48
+ fs.closeSync(fd);
49
+ }
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Reconstructs the event stream from a run directory. A malformed line is
55
+ * skipped, not fatal — a truncated final line is the normal wreckage of a
56
+ * crash, and the events before it are exactly what survived. A missing file
57
+ * yields an empty stream for the same reason: history readers must never
58
+ * throw on the artifacts a crash leaves behind.
59
+ */
60
+ export function readEvents(runDirPath) {
61
+ let text;
62
+ try {
63
+ text = fs.readFileSync(eventsFilePath(runDirPath), "utf8");
64
+ }
65
+ catch {
66
+ logger.debug(`run store: no events file in ${runDirPath}`);
67
+ return [];
68
+ }
69
+ const events = [];
70
+ const lines = text.split(/\r?\n/);
71
+ for (const [index, line] of lines.entries()) {
72
+ if (line.trim() === "") {
73
+ continue;
74
+ }
75
+ try {
76
+ const parsed = JSON.parse(line);
77
+ if (isEventLike(parsed)) {
78
+ events.push(parsed);
79
+ }
80
+ else {
81
+ logger.debug(`run store: line ${index + 1} in ${runDirPath} is not an event`);
82
+ }
83
+ }
84
+ catch {
85
+ logger.debug(`run store: skipping malformed line ${index + 1} in ${runDirPath}`);
86
+ }
87
+ }
88
+ return events;
89
+ }
90
+ /**
91
+ * Rebuilds a summary from events alone. The stream's `RunCompleted` numbers
92
+ * (A0: taken straight from the result message) are authoritative when
93
+ * present; the event-derived fallbacks exist so a crashed run still gets a
94
+ * meaningful summary — that is the entire point of rebuilding rather than
95
+ * trusting a file the crash prevented us from writing.
96
+ */
97
+ export function summarize(events) {
98
+ let state = "CRASHED";
99
+ let turns = 0;
100
+ let resultTurns = 0;
101
+ let resultDurationMs = 0;
102
+ let durationMs = 0;
103
+ let tokensIn = 0;
104
+ let tokensOut = 0;
105
+ let denied = 0;
106
+ let retries = 0;
107
+ let validation = "none";
108
+ const filesChanged = new Set();
109
+ for (const event of events) {
110
+ switch (event.type) {
111
+ case "TurnStarted":
112
+ turns = Math.max(turns, event.turn);
113
+ break;
114
+ case "FileChanged":
115
+ filesChanged.add(event.path);
116
+ break;
117
+ case "ToolDenied":
118
+ denied += 1;
119
+ if (validation === "pending") {
120
+ validation = "denied";
121
+ }
122
+ break;
123
+ case "ApiRetry":
124
+ retries += 1;
125
+ break;
126
+ case "ValidationStarted":
127
+ validation = "pending";
128
+ break;
129
+ case "ValidationCompleted":
130
+ validation = event.ok ? "ok" : "failed";
131
+ break;
132
+ case "RunCompleted":
133
+ state = "COMPLETED";
134
+ // A run can carry MORE THAN ONE result message: a child that hits
135
+ // --max-turns and continues emits one per segment. Observed live on
136
+ // 2026-09-20 — RunFailed(max_turns), then two RunCompleted — where
137
+ // overwriting made a 21-minute, 64-turn run persist as "88s, 5 turns".
138
+ // Each result describes only its own segment, so accumulate, and let
139
+ // the derived turn count win when it is larger.
140
+ resultTurns += event.turns;
141
+ resultDurationMs += event.durationMs;
142
+ tokensIn += event.tokensIn;
143
+ tokensOut += event.tokensOut;
144
+ break;
145
+ case "RunFailed":
146
+ state = "FAILED";
147
+ break;
148
+ case "RunCancelled":
149
+ state = "CANCELLED";
150
+ break;
151
+ default:
152
+ break;
153
+ }
154
+ }
155
+ // The wall clock is the span of the stream; a sum of per-segment result
156
+ // durations misses the gaps between them. Take whichever is larger so a
157
+ // long run can never be reported as a short one.
158
+ const span = events.length > 0
159
+ ? Date.parse(events[events.length - 1].ts) - Date.parse(events[0].ts)
160
+ : 0;
161
+ durationMs = Math.max(resultDurationMs, Number.isFinite(span) && span > 0 ? span : 0);
162
+ turns = Math.max(turns, resultTurns);
163
+ return {
164
+ id: events.length > 0 ? events[0].runId : "",
165
+ state,
166
+ turns,
167
+ durationMs,
168
+ filesChanged: [...filesChanged],
169
+ tokensIn,
170
+ tokensOut,
171
+ denied,
172
+ retries,
173
+ validation,
174
+ };
175
+ }
176
+ /**
177
+ * The runtime check is deliberately just "parses and has a `type` string":
178
+ * fully validating the union at runtime would duplicate the event model, and
179
+ * a line that passes this check but carries wrong fields is history written
180
+ * by a future version — dropping it would lose data forever.
181
+ */
182
+ function isEventLike(value) {
183
+ return (typeof value === "object" &&
184
+ value !== null &&
185
+ typeof value.type === "string");
186
+ }
@@ -0,0 +1,112 @@
1
+ import { randomBytes } from "node:crypto";
2
+ /**
3
+ * ULID generation (specs/v2-architecture.md, Phase B).
4
+ *
5
+ * Lexicographic sort order equals chronological order — that property, not
6
+ * the id's shape, is why runs use ULIDs instead of UUIDs: the run history is
7
+ * browsed by sorting ids as plain strings.
8
+ */
9
+ /** Crockford base32: no I, L, O or U, so no visually ambiguous character ever appears in a run id. */
10
+ const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
11
+ const TIME_LEN = 10;
12
+ const RANDOM_BYTES = 10; // 80 bits = exactly 16 base32 chars × 5 bits
13
+ /**
14
+ * Process-wide monotonic state: the last timestamp used and the random
15
+ * component it was paired with. Two ids in the same millisecond must still
16
+ * order, so the second one increments the first one's randomness instead of
17
+ * drawing fresh bytes.
18
+ */
19
+ let lastTime = -1;
20
+ let lastRandom = null;
21
+ /**
22
+ * A 26-char ULID: 10 chars of millisecond timestamp (most significant first)
23
+ * followed by 16 chars of randomness. Monotonic within the process — same-ms
24
+ * calls increment the 80-bit random component as a big-endian integer, so
25
+ * generated ids never collide and always sort after their predecessors.
26
+ * `now` is injectable so tests can pin and freeze time.
27
+ */
28
+ export function ulid(now = Date.now) {
29
+ let time = now();
30
+ let random = null;
31
+ // `<=` rather than `===` also absorbs a clock that jumps backwards: reusing
32
+ // the last timestamp with an incremented random part preserves the invariant
33
+ // that actually matters — generation order == sort order.
34
+ if (lastRandom !== null && time <= lastTime) {
35
+ random = incrementRandom(lastRandom);
36
+ if (random === null) {
37
+ // All 80 random bits overflowed (probability 2^-80). Spin to the next
38
+ // millisecond rather than return a value that sorts before its
39
+ // predecessor; if the clock never advances (a frozen injected clock),
40
+ // step the timestamp ourselves — still strictly after the previous id.
41
+ let spins = 0;
42
+ do {
43
+ time = now();
44
+ } while (time <= lastTime && ++spins < 1_000_000);
45
+ if (time <= lastTime) {
46
+ time = lastTime + 1;
47
+ }
48
+ random = randomBytes(RANDOM_BYTES);
49
+ }
50
+ else {
51
+ time = lastTime;
52
+ }
53
+ }
54
+ else {
55
+ random = randomBytes(RANDOM_BYTES);
56
+ }
57
+ lastTime = time;
58
+ lastRandom = random;
59
+ return encodeTime(time) + encodeRandom(random);
60
+ }
61
+ /** `run_` + a ULID (doc §5): the prefix makes run ids self-describing in logs, dirs and file names. */
62
+ export function runId(now = Date.now) {
63
+ return `run_${ulid(now)}`;
64
+ }
65
+ /**
66
+ * A 48-bit millisecond timestamp fits exactly in a double, so plain division
67
+ * encodes it most-significant-first into 10 chars — which is what makes the
68
+ * time part of the string sort chronologically.
69
+ */
70
+ function encodeTime(time) {
71
+ let chars = "";
72
+ let remaining = time;
73
+ for (let count = 0; count < TIME_LEN; count++) {
74
+ chars = ENCODING[remaining % 32] + chars;
75
+ remaining = Math.floor(remaining / 32);
76
+ }
77
+ return chars;
78
+ }
79
+ /**
80
+ * 10 bytes → 16 × 5-bit symbols, big-endian. 80 is a multiple of 5, so the
81
+ * bit funnel drains exactly with nothing left over.
82
+ */
83
+ function encodeRandom(bytes) {
84
+ let chars = "";
85
+ let buffer = 0;
86
+ let bits = 0;
87
+ for (const byte of bytes) {
88
+ buffer = (buffer << 8) | byte;
89
+ bits += 8;
90
+ while (bits >= 5) {
91
+ bits -= 5;
92
+ chars += ENCODING[(buffer >>> bits) & 31];
93
+ }
94
+ }
95
+ return chars;
96
+ }
97
+ /**
98
+ * +1 on the 80-bit random component read as a big-endian integer. Returns
99
+ * null when every bit was already 1 — the caller must then move to a new
100
+ * millisecond, because a wrapped value would sort before its predecessor.
101
+ */
102
+ function incrementRandom(bytes) {
103
+ const next = Uint8Array.from(bytes);
104
+ for (let index = next.length - 1; index >= 0; index--) {
105
+ if (next[index] < 0xff) {
106
+ next[index] += 1;
107
+ return next;
108
+ }
109
+ next[index] = 0;
110
+ }
111
+ return null;
112
+ }