mobygate 0.8.0 → 0.8.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,178 @@ All notable changes to mobygate are documented here. Format loosely follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version numbers are
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.8.2] — 2026-04-28
8
+
9
+ Multi-agent fixes. Found the day after v0.8.1 shipped, while testing
10
+ three OpenClaw bots (Mobius/Lux/Mercury) in parallel on the same
11
+ machine. Both bugs were invisible without the v0.8.1 inspector.
12
+
13
+ ### Fixed
14
+
15
+ - **Session-key collision when multiple agents share a boilerplate
16
+ prefix in their system prompt.** v0.7.1's auto-derive hashed the
17
+ first 500 characters of the system prompt; OpenClaw's "You are a
18
+ personal assistant running inside OpenClaw…" preamble fills more
19
+ than that, so the per-agent personality content (loaded later from
20
+ workspace SOUL.md / IDENTITY.md / etc.) didn't reach the hash. Two
21
+ separate agents (Lux on sonnet-4-6, Mercury on sonnet-4-6) collided
22
+ onto the same auto-key when given the same first user message
23
+ ("@Lux @Mercury Hi"). Same key → same SDK session reuse → cache
24
+ thrash and potential session-state mixing.
25
+
26
+ Bumped `SYSTEM_TRIM` from 500 → 20000 chars. Verified against real
27
+ captured request bodies that collided in v0.8.1 — they now hash to
28
+ distinct keys (`auto_b0371e5c…` vs `auto_2b90afd7…`).
29
+
30
+ SHA-256 cost on 20kB is ~10-20µs per request, irrelevant in the
31
+ hot path.
32
+
33
+ - **Model map silently downgraded `claude-sonnet-4-6` to retired
34
+ `claude-sonnet-4-5-20250929`.** When the v0.8.0 model map was
35
+ written, the Claude Agent SDK didn't recognize the un-dated
36
+ `claude-sonnet-4-6` alias and we worked around it by routing to the
37
+ most recent dated 4-5. The SDK has since added native 4-6 support,
38
+ but mobygate kept the workaround in place. Result: clients (OpenClaw
39
+ Lux/Mercury) configured for sonnet-4-6 were having their requests
40
+ rewritten to the retired 4-5-20250929 dated id. Anthropic accepted
41
+ the call but the response wasn't billing into the user's "Sonnet
42
+ only" quota — it was showing 0% used despite live traffic. Likely
43
+ Claude was falling back internally to opus or returning a
44
+ zero-billed degraded response.
45
+
46
+ Fix: route `claude-sonnet-4-6` through directly. Also updated
47
+ `claude-sonnet-4` and the `sonnet` shorthand to point at 4-6
48
+ (current latest) instead of the retired dated 4-5 entry. Explicit
49
+ `claude-sonnet-4-5` requests still route to the dated id for
50
+ backward compatibility.
51
+
52
+ Discovery: the inspector showed Lux/Mercury captures all stamped
53
+ with `model: claude-sonnet-4-6` (correct from the request side) but
54
+ Anthropic's quota panel reported 0% sonnet usage. The server.log's
55
+ `model=claude-sonnet-4-6 → claude-sonnet-4-5-20250929` translation
56
+ line was the smoking gun.
57
+
58
+ ### Notes
59
+
60
+ The proper long-term fix is for clients to pass an explicit
61
+ `X-Session-Id` header per agent (mobygate has supported this since
62
+ v0.7.1 — it always wins over auto-derive). This bump is a defensive
63
+ measure for clients that don't.
64
+
65
+ Discovery flow is a nice validation of the v0.8.1 inspector: the
66
+ collision was invisible at the OpenClaw level (each bot's replies
67
+ arrived correctly because OpenClaw maintains its own per-agent SDK
68
+ state) but jumped out as soon as the captures were sorted by session
69
+ key in the inspector — two different model requests with the same
70
+ session-key, with bootstrap text 55kB long but identical first 500
71
+ chars. Without the inspector, this would have surfaced as
72
+ unpredictable cache hit rates and been blamed on Anthropic.
73
+
74
+ ## [0.8.1] — 2026-04-27
75
+
76
+ Diagnostic visibility release. Adds a request/response capture system,
77
+ a session inspector UI, a `mobygate tail` CLI, and fixes the v0.8.0
78
+ connector idempotency bug. Born out of an OpenClaw debugging session
79
+ where it took an hour of forensics to discover OpenClaw was using
80
+ OpenAI shape (no cache_control) instead of `/v1/messages` — a one-line
81
+ config flip — because mobygate had no visibility into what its
82
+ upstream clients were actually sending.
83
+
84
+ ### Added
85
+
86
+ - **Request/response capture system** (`lib/request-capture.js`).
87
+ - Off by default. Three ways to enable:
88
+ 1. Env var: `MOBY_CAPTURE=1 mobygate start`
89
+ 2. Touch file: `touch ~/.mobygate/.capture-enabled` (survives
90
+ restarts, dashboard-toggleable)
91
+ 3. Dashboard inspector toggle button
92
+ - Writes paired files per request to `~/.mobygate/captures/`:
93
+ - `{ts}_{path}_{requestId}.json` — raw inbound body
94
+ - `{ts}_{path}_{requestId}.summary.txt` — human-readable analysis
95
+ (system blocks, cache_control markers, message timeline, tool
96
+ definitions, token breakdown)
97
+ - On response: appends actual usage from the SDK including
98
+ `cache_read_input_tokens`, `cache_creation_input_tokens`,
99
+ `input_tokens`, `output_tokens`, plus computed cache hit % and
100
+ "savings vs uncached" estimate.
101
+ - Auto-rotation: keeps last `MOBY_CAPTURE_KEEP=100` captures (200
102
+ files), prunes oldest in the background.
103
+
104
+ - **Session inspector UI** at `/inspector`. New focused dashboard for
105
+ browsing captures:
106
+ - List view: all recent captures sorted newest-first, one-line stats
107
+ (timestamp, path, model, msgs, tokens, cache hit %), live polling
108
+ every 3s.
109
+ - Detail view: full request body decoded — system blocks (with
110
+ cache_control highlighted in green), messages timeline (color-coded
111
+ by role, tool_use/tool_result expanded with previews), tool
112
+ definitions, response usage stats.
113
+ - Capture toggle in header — flips the touch file live without
114
+ restarting mobygate.
115
+ - Linked from the main dashboard's endpoints row.
116
+
117
+ - **`mobygate tail` CLI** — `tail -f` style live view of captures as
118
+ they land. Shows last 10 historical entries, then watches the
119
+ captures dir for new files. Color-codes `/v1/messages` (green,
120
+ native shape) vs `/v1/chat/completions` (yellow, OpenAI shape) so
121
+ shape mismatches jump off the screen.
122
+
123
+ - **`extractSdkUsage()` helper** in `lib/anthropic.js` — defensive
124
+ extraction of cache + token fields from SDK result messages,
125
+ handles both flat (`message.input_tokens`) and nested
126
+ (`message.usage.input_tokens`) shapes from the Claude Agent SDK.
127
+ Used by all 4 mobygate handlers (the OpenAI streaming handler also
128
+ now tracks usage where it didn't before).
129
+
130
+ - **Connector migration detection** — `openclawConnector.inspect()`
131
+ now reports "shadow providers": entries in OpenClaw's config that
132
+ point at mobygate's base URL but aren't registered under our
133
+ canonical `moby` / `moby-native` names. Catches pre-v0.8.0
134
+ hand-rolled `claude-max-proxy`-style configs (which silently bypass
135
+ the native surface). Returned as `shadowProviders: [{ name, api,
136
+ baseUrl, recommendation }]` in inspect output.
137
+
138
+ - **Cache endpoints for the dashboard:**
139
+ - `GET /dashboard/captures` — list captures with summary stats
140
+ - `GET /dashboard/captures/:filename` — full body + summary for one
141
+ capture
142
+ - `GET /dashboard/captures-state` — current toggle state
143
+ - `POST /dashboard/captures-toggle` — flip toggle live
144
+ - All require local-origin (DNS-rebinding protection).
145
+
146
+ ### Fixed
147
+
148
+ - **Connector idempotency** (v0.8.0 OPEN bug). `mobygate connect <id>`
149
+ run twice with no real change was producing spurious "(changed)"
150
+ diffs and rewriting byte-identical files. Root cause: `diffSummary`
151
+ walks objects structurally (sensitive to key order); the actual
152
+ serialized JSON output was identical. Fix: `writeConfigSafe` now
153
+ byte-compares the rendered output against the existing file content
154
+ before deciding to write. Returns `unchanged: true` if no real
155
+ change. Both `hermesConnector.apply()` and
156
+ `openclawConnector.apply()` updated to surface this flag.
157
+
158
+ - **Tool name display in capture analyzer**. OpenAI-shape tools nest
159
+ the name under `tool.function.name`; the analyzer was only checking
160
+ `tool.name` and showing `(unnamed)` for everything. Now handles
161
+ both shapes via the new `toolName(t)` helper.
162
+
163
+ ### Notes
164
+
165
+ The capture system was the lever that made the entire OpenClaw
166
+ debugging session productive. Without it we had no visibility into
167
+ which wire format clients were using, whether `cache_control` markers
168
+ were being set, where in the request the breakpoints landed, or how
169
+ the conversation was growing turn-over-turn. With it, the diagnosis
170
+ took 60 seconds: "this is `/v1/chat/completions`, no `system` array,
171
+ no cache markers, 33k tokens per turn."
172
+
173
+ The migration detection in `openclawConnector.inspect()` is a small
174
+ prevent-the-next-occurrence add. If `inspect()` had reported
175
+ `claude-max-proxy` as a shadow provider with a "flip api to
176
+ anthropic-messages" recommendation, today's hour-long forensics would
177
+ have been a single CLI command.
178
+
7
179
  ## [0.8.0] — 2026-04-25
8
180
 
9
181
  Auto-wire third-party clients to use mobygate. The "find your client's
package/bin/mobygate.js CHANGED
@@ -480,6 +480,78 @@ function cmdLogs() {
480
480
  spawnSync(cmd, args, { stdio: 'inherit' });
481
481
  }
482
482
 
483
+ // `mobygate tail` — watch captures live, print a one-line summary as
484
+ // each new request lands. Reads the .summary.txt file paired with each
485
+ // new .json file in ~/.mobygate/captures/.
486
+ async function cmdTail() {
487
+ const { homedir } = await import('os');
488
+ const { watch, existsSync, readFileSync, mkdirSync, statSync } = await import('fs');
489
+ const { readdir } = await import('fs/promises');
490
+ const { join } = await import('path');
491
+
492
+ const dir = process.env.MOBYGATE_CAPTURE_DIR
493
+ || join(process.env.MOBYGATE_HOME || join(homedir(), '.mobygate'), 'captures');
494
+
495
+ if (!existsSync(dir)) {
496
+ print(`Captures directory does not exist yet: ${dir}`);
497
+ print(`Enable capture (touch ~/.mobygate/.capture-enabled OR set MOBY_CAPTURE=1) and send a request.`);
498
+ return;
499
+ }
500
+
501
+ // Print recent history first (last 10), then watch.
502
+ const initial = (await readdir(dir))
503
+ .filter(n => n.endsWith('.summary.txt'))
504
+ .map(n => ({ name: n, mtime: statSync(join(dir, n)).mtimeMs }))
505
+ .sort((a, b) => a.mtime - b.mtime)
506
+ .slice(-10);
507
+
508
+ for (const { name } of initial) {
509
+ printSummaryLine(join(dir, name));
510
+ }
511
+
512
+ print(''); // blank line separator before live tail
513
+ print('--- watching for new captures (Ctrl-C to stop) ---');
514
+
515
+ const seen = new Set(initial.map(x => x.name));
516
+ watch(dir, (_event, name) => {
517
+ if (!name || !name.endsWith('.summary.txt')) return;
518
+ if (seen.has(name)) return;
519
+ seen.add(name);
520
+ // Wait briefly so the file is flushed (fs.watch can fire on the
521
+ // creation rename before content is fully written).
522
+ setTimeout(() => printSummaryLine(join(dir, name)), 50);
523
+ });
524
+ }
525
+
526
+ function printSummaryLine(summaryPath) {
527
+ try {
528
+ const text = readFileSync(summaryPath, 'utf8');
529
+ const grab = (re) => { const m = text.match(re); return m ? m[1].trim() : '?'; };
530
+ const ts = grab(/timestamp:\s+(\S+)/).slice(11, 19);
531
+ const path = grab(/^path:\s+(\S+)/m);
532
+ const model = grab(/^model:\s+(\S+)/m);
533
+ const msgs = grab(/^messages:\s+(\d+)/m);
534
+ const tokens = grab(/grand total:\s+(\d+)/);
535
+ const hit = grab(/cache hit rate:\s+([\d.]+)%/);
536
+ const status = grab(/^status:\s+(\S+)/m);
537
+ const dur = grab(/^duration:\s+(\d+) ms/m);
538
+
539
+ const hitDisplay = hit === '?' ? '─' : `${hit}% hit`;
540
+ const statusDisplay = status === '?' ? '(in flight)' : `${status} ${dur}ms`;
541
+
542
+ // Color-code the path: green for /v1/messages (native), yellow for /v1/chat/completions
543
+ const isNative = path === '/v1/messages';
544
+ const pathColored = isNative ? `\x1b[32m${path}\x1b[0m` : `\x1b[33m${path}\x1b[0m`;
545
+ const hitColored = hit === '?' ? hitDisplay
546
+ : parseFloat(hit) > 50 ? `\x1b[32m${hitDisplay}\x1b[0m`
547
+ : `\x1b[33m${hitDisplay}\x1b[0m`;
548
+
549
+ print(`${ts} ${pathColored.padEnd(35)} ${model.padEnd(22)} msgs=${msgs.padEnd(4)} tok=${tokens.padEnd(6)} ${hitColored.padEnd(20)} ${statusDisplay}`);
550
+ } catch (e) {
551
+ print(`(failed to read ${summaryPath}: ${e.message})`);
552
+ }
553
+ }
554
+
483
555
  async function cmdAuth() {
484
556
  section('mobygate auth');
485
557
  const status = await getAuthStatus();
@@ -869,6 +941,7 @@ Usage:
869
941
  mobygate restart Stop + start
870
942
  mobygate status Show service + auth + /health state
871
943
  mobygate logs Tail the server log
944
+ mobygate tail Watch captures live (request/response inspector)
872
945
  mobygate auth Show auth status + run a refresh probe
873
946
  mobygate connect Auto-wire detected clients (Hermes, OpenClaw) to
874
947
  use mobygate. Add: <id> for one client, --all for
@@ -900,6 +973,7 @@ const COMMANDS = {
900
973
  restart: cmdRestart,
901
974
  status: cmdStatus,
902
975
  logs: cmdLogs,
976
+ tail: cmdTail,
903
977
  auth: cmdAuth,
904
978
  uninstall: cmdUninstall,
905
979
  version: cmdVersion,
package/index.html CHANGED
@@ -361,6 +361,7 @@
361
361
  <a href="/auth/status?quick=1" class="py-[3px] px-2.5 border border-[#2A2A1F] text-[#C9D9A8] text-[11px] leading-[14px] hover:border-[#5A5F54]">/auth/status</a>
362
362
  <a href="/v1/models" class="py-[3px] px-2.5 border border-[#2A2A1F] text-[#C9D9A8] text-[11px] leading-[14px] hover:border-[#5A5F54]">/v1/models</a>
363
363
  <a href="/events" class="py-[3px] px-2.5 border border-[#2A2A1F] text-[#C9D9A8] text-[11px] leading-[14px] hover:border-[#5A5F54]">/events</a>
364
+ <a href="/inspector" class="py-[3px] px-2.5 border border-[#B7E56D] text-[#B7E56D] text-[11px] leading-[14px] hover:bg-[#1A1F12]">/inspector</a>
364
365
  </div>
365
366
  <div class="flex items-center gap-3">
366
367
  <span class="uppercase text-[#5A5F54] text-[10px] leading-3 tracking-[0.12em]" id="f-stream">stream · connecting</span>