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 +172 -0
- package/bin/mobygate.js +74 -0
- package/index.html +1 -0
- package/inspector.html +422 -0
- package/lib/anthropic.js +23 -0
- package/lib/connectors/hermes.js +3 -1
- package/lib/connectors/openclaw.js +39 -2
- package/lib/connectors/safety.js +18 -1
- package/lib/request-capture.js +394 -0
- package/lib/session-derive.js +20 -1
- package/package.json +2 -1
- package/server.js +263 -10
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>
|