@moxxy/sdk 0.14.2 → 0.14.4
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/dist/compactor-helpers.d.ts.map +1 -1
- package/dist/compactor-helpers.js +9 -5
- package/dist/compactor-helpers.js.map +1 -1
- package/dist/mode/collect-stream.d.ts +68 -0
- package/dist/mode/collect-stream.d.ts.map +1 -0
- package/dist/mode/collect-stream.js +181 -0
- package/dist/mode/collect-stream.js.map +1 -0
- package/dist/mode/project-messages.d.ts +84 -0
- package/dist/mode/project-messages.d.ts.map +1 -0
- package/dist/mode/project-messages.js +392 -0
- package/dist/mode/project-messages.js.map +1 -0
- package/dist/mode/single-shot.d.ts +17 -0
- package/dist/mode/single-shot.d.ts.map +1 -0
- package/dist/mode/single-shot.js +53 -0
- package/dist/mode/single-shot.js.map +1 -0
- package/dist/mode/stable-hash.d.ts +7 -0
- package/dist/mode/stable-hash.d.ts.map +1 -0
- package/dist/mode/stable-hash.js +20 -0
- package/dist/mode/stable-hash.js.map +1 -0
- package/dist/mode/stuck-loop.d.ts +39 -0
- package/dist/mode/stuck-loop.d.ts.map +1 -0
- package/dist/mode/stuck-loop.js +51 -0
- package/dist/mode/stuck-loop.js.map +1 -0
- package/dist/mode-helpers.d.ts +15 -175
- package/dist/mode-helpers.d.ts.map +1 -1
- package/dist/mode-helpers.js +14 -666
- package/dist/mode-helpers.js.map +1 -1
- package/dist/tunnel.d.ts.map +1 -1
- package/dist/tunnel.js +11 -1
- package/dist/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/src/compactor-helpers.test.ts +21 -0
- package/src/compactor-helpers.ts +10 -5
- package/src/mode/collect-stream.ts +247 -0
- package/src/mode/project-messages.test.ts +121 -0
- package/src/mode/project-messages.ts +461 -0
- package/src/mode/single-shot.ts +63 -0
- package/src/mode/stable-hash.ts +20 -0
- package/src/mode/stuck-loop.ts +89 -0
- package/src/mode-helpers.ts +32 -850
- package/src/mode.test.ts +20 -0
- package/src/token-accounting.test.ts +90 -0
- package/src/tunnel.test.ts +21 -0
- package/src/tunnel.ts +10 -1
package/src/mode.test.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { migrateModeName } from './mode.js';
|
|
3
|
+
|
|
4
|
+
describe('migrateModeName', () => {
|
|
5
|
+
it('maps every legacy mode name to its current target', () => {
|
|
6
|
+
expect(migrateModeName('tool-use')).toBe('default');
|
|
7
|
+
expect(migrateModeName('deep-research')).toBe('research');
|
|
8
|
+
expect(migrateModeName('plan-execute')).toBe('default');
|
|
9
|
+
expect(migrateModeName('bmad')).toBe('default');
|
|
10
|
+
expect(migrateModeName('developer')).toBe('default');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('passes a current/unknown name through unchanged (identity)', () => {
|
|
14
|
+
expect(migrateModeName('default')).toBe('default');
|
|
15
|
+
expect(migrateModeName('goal')).toBe('goal');
|
|
16
|
+
expect(migrateModeName('research')).toBe('research');
|
|
17
|
+
expect(migrateModeName('some-future-mode')).toBe('some-future-mode');
|
|
18
|
+
expect(migrateModeName('')).toBe('');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
asEventId,
|
|
5
5
|
asSessionId,
|
|
6
6
|
asTurnId,
|
|
7
|
+
summarizeSessionTokensFromEvents,
|
|
7
8
|
summarizeTokensByModel,
|
|
8
9
|
type ModelUsageTotals,
|
|
9
10
|
type MoxxyEvent,
|
|
@@ -78,6 +79,95 @@ describe('summarizeTokensByModel', () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
describe('summarizeSessionTokensFromEvents — cost fold', () => {
|
|
83
|
+
it('counts only responses that reported usage', () => {
|
|
84
|
+
const s = summarizeSessionTokensFromEvents([
|
|
85
|
+
resp(0, {}), // no token fields → not counted
|
|
86
|
+
resp(1, { inputTokens: 100, outputTokens: 10 }),
|
|
87
|
+
resp(2, { cacheReadTokens: 50 }),
|
|
88
|
+
]);
|
|
89
|
+
expect(s.calls).toBe(2);
|
|
90
|
+
expect(s.totalInput).toBe(100);
|
|
91
|
+
expect(s.totalCacheRead).toBe(50);
|
|
92
|
+
expect(s.totalOutput).toBe(10);
|
|
93
|
+
expect(s.totalPrompt).toBe(150);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('applies the read 0.1x / write 1.25x billing multipliers', () => {
|
|
97
|
+
const s = summarizeSessionTokensFromEvents([
|
|
98
|
+
resp(0, { inputTokens: 100, cacheReadTokens: 1000, cacheCreationTokens: 200 }),
|
|
99
|
+
]);
|
|
100
|
+
// totalPrompt = 100 + 1000 + 200 = 1300 (== uncachedInputEq)
|
|
101
|
+
expect(s.totalPrompt).toBe(1300);
|
|
102
|
+
expect(s.uncachedInputEq).toBe(1300);
|
|
103
|
+
// billedInputEq = 100 + 1000*0.1 + 200*1.25 = 100 + 100 + 250 = 450
|
|
104
|
+
expect(s.billedInputEq).toBeCloseTo(450, 6);
|
|
105
|
+
expect(s.cacheHitRate).toBeCloseTo(1000 / 1300, 6);
|
|
106
|
+
expect(s.savedRatio).toBeCloseTo(1 - 450 / 1300, 6);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('guards divide-by-zero when no prompt tokens were reported', () => {
|
|
110
|
+
const s = summarizeSessionTokensFromEvents([resp(0, { outputTokens: 5 })]);
|
|
111
|
+
expect(s.totalPrompt).toBe(0);
|
|
112
|
+
expect(s.cacheHitRate).toBe(0);
|
|
113
|
+
expect(s.savedRatio).toBe(0);
|
|
114
|
+
// No writes → cache considered effective (deliberately-off cache never alarms).
|
|
115
|
+
expect(s.cacheEffective).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reports cacheEffective=true for a healthy cache (high read ratio)', () => {
|
|
119
|
+
const events = Array.from({ length: 6 }, (_, i) =>
|
|
120
|
+
resp(i, { inputTokens: 10, cacheReadTokens: 1000, cacheCreationTokens: 100 }),
|
|
121
|
+
);
|
|
122
|
+
const s = summarizeSessionTokensFromEvents(events);
|
|
123
|
+
expect(s.calls).toBe(6);
|
|
124
|
+
expect(s.cacheHitRate).toBeGreaterThan(0.05);
|
|
125
|
+
expect(s.savedRatio).toBeGreaterThan(0);
|
|
126
|
+
expect(s.cacheEffective).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('trips cacheEffective=false on broken cache: >=5 calls, writes>0, near-zero reads', () => {
|
|
130
|
+
// 5 calls each writing cache but never reading it → cacheHitRate 0.
|
|
131
|
+
const events = Array.from({ length: 5 }, (_, i) =>
|
|
132
|
+
resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
|
|
133
|
+
);
|
|
134
|
+
const s = summarizeSessionTokensFromEvents(events);
|
|
135
|
+
expect(s.calls).toBe(5);
|
|
136
|
+
expect(s.totalCacheCreation).toBeGreaterThan(0);
|
|
137
|
+
expect(s.cacheHitRate).toBe(0);
|
|
138
|
+
expect(s.cacheEffective).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('exempts the broken-cache trip below 5 calls (boundary at calls===4 vs 5)', () => {
|
|
142
|
+
const make = (n: number) =>
|
|
143
|
+
summarizeSessionTokensFromEvents(
|
|
144
|
+
Array.from({ length: n }, (_, i) =>
|
|
145
|
+
resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
expect(make(4).cacheEffective).toBe(true); // < 5 → exempt
|
|
149
|
+
expect(make(5).cacheEffective).toBe(false); // >= 5 → trips
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('exempts when there are no cache writes even with zero reads over many calls', () => {
|
|
153
|
+
const events = Array.from({ length: 8 }, (_, i) =>
|
|
154
|
+
resp(i, { inputTokens: 100, cacheCreationTokens: 0, cacheReadTokens: 0 }),
|
|
155
|
+
);
|
|
156
|
+
expect(summarizeSessionTokensFromEvents(events).cacheEffective).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not trip when cacheHitRate sits exactly at the 0.05 threshold', () => {
|
|
160
|
+
// hitRate === 0.05 is NOT < 0.05, so the cache stays effective.
|
|
161
|
+
// read/(input+read+write) = 125 / 2500 = 0.05.
|
|
162
|
+
const events = Array.from({ length: 5 }, (_, i) =>
|
|
163
|
+
resp(i, { inputTokens: 1875, cacheReadTokens: 125, cacheCreationTokens: 500 }),
|
|
164
|
+
);
|
|
165
|
+
const s = summarizeSessionTokensFromEvents(events);
|
|
166
|
+
expect(s.cacheHitRate).toBeCloseTo(0.05, 6);
|
|
167
|
+
expect(s.cacheEffective).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
81
171
|
describe('addModelTotals', () => {
|
|
82
172
|
it('adds field-wise', () => {
|
|
83
173
|
const a: ModelUsageTotals = { calls: 1, inputTokens: 10, outputTokens: 2, cacheReadTokens: 3, cacheCreationTokens: 4 };
|
package/src/tunnel.test.ts
CHANGED
|
@@ -29,6 +29,27 @@ describe('spawnCliTunnel', () => {
|
|
|
29
29
|
await handle.close();
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
it('u125-1: resolves a URL split across two stdout chunks', async () => {
|
|
33
|
+
// Write the URL in two separate process.stdout.write calls, split
|
|
34
|
+
// mid-host, with a tick gap so they surface as distinct 'data' events.
|
|
35
|
+
// Matching each chunk in isolation would miss it; the rolling buffer must
|
|
36
|
+
// stitch them.
|
|
37
|
+
const handle = await spawnCliTunnel({
|
|
38
|
+
cmd: process.execPath,
|
|
39
|
+
args: [
|
|
40
|
+
'-e',
|
|
41
|
+
'process.stdout.write("ready https://abc-1");' +
|
|
42
|
+
'setTimeout(()=>{process.stdout.write("23.example.test\\n");},50);' +
|
|
43
|
+
'setInterval(()=>{}, 1000);',
|
|
44
|
+
],
|
|
45
|
+
urlRegex: URL_RE,
|
|
46
|
+
name: 'splittunnel',
|
|
47
|
+
timeoutMs: 5_000,
|
|
48
|
+
});
|
|
49
|
+
expect(handle.url).toBe('https://abc-123.example.test');
|
|
50
|
+
await handle.close();
|
|
51
|
+
});
|
|
52
|
+
|
|
32
53
|
it('rejects when the child exits before emitting a URL', async () => {
|
|
33
54
|
await expect(
|
|
34
55
|
spawnCliTunnel({
|
package/src/tunnel.ts
CHANGED
|
@@ -158,11 +158,20 @@ export function spawnCliTunnel(opts: SpawnCliTunnelOptions): Promise<CliTunnelHa
|
|
|
158
158
|
}, timeoutMs);
|
|
159
159
|
timer.unref?.();
|
|
160
160
|
|
|
161
|
+
// 'data' events are not line-buffered: the assigned URL can straddle two
|
|
162
|
+
// chunk boundaries (large URL, fragmented flush). Match against a rolling
|
|
163
|
+
// accumulation rather than each lone chunk, capped so a chatty tunnel that
|
|
164
|
+
// never prints a URL can't grow this unboundedly while we wait.
|
|
165
|
+
const MAX_BUF = 8192;
|
|
166
|
+
let acc = '';
|
|
161
167
|
const onData = (buf: Buffer): void => {
|
|
162
168
|
if (settled) return; // drain quietly once resolved so the pipe never fills
|
|
163
|
-
|
|
169
|
+
acc += buf.toString('utf8');
|
|
170
|
+
if (acc.length > MAX_BUF) acc = acc.slice(acc.length - MAX_BUF);
|
|
171
|
+
const url = urlRegex.exec(acc)?.[0] ?? null;
|
|
164
172
|
if (!url) return;
|
|
165
173
|
settled = true;
|
|
174
|
+
acc = '';
|
|
166
175
|
clearTimeout(timer);
|
|
167
176
|
resolve({ url, pid: child.pid ?? -1, close: untrack });
|
|
168
177
|
};
|