@moxxy/sdk 0.14.3 → 0.14.5
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/README.md +2 -0
- package/dist/compactor-helpers.d.ts +59 -0
- package/dist/compactor-helpers.d.ts.map +1 -1
- package/dist/compactor-helpers.js +81 -6
- package/dist/compactor-helpers.js.map +1 -1
- package/dist/elision-helpers.d.ts.map +1 -1
- package/dist/elision-helpers.js +12 -5
- package/dist/elision-helpers.js.map +1 -1
- package/dist/elision-state.d.ts +4 -0
- package/dist/elision-state.d.ts.map +1 -1
- package/dist/elision-state.js +41 -0
- package/dist/elision-state.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/mode/project-messages.d.ts +9 -0
- package/dist/mode/project-messages.d.ts.map +1 -1
- package/dist/mode/project-messages.js +4 -1
- package/dist/mode/project-messages.js.map +1 -1
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +21 -0
- package/dist/server.js.map +1 -0
- package/dist/transcriber.d.ts +13 -0
- package/dist/transcriber.d.ts.map +1 -1
- package/dist/transcriber.js +13 -1
- package/dist/transcriber.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 +9 -1
- package/src/compactor-helpers.test.ts +127 -0
- package/src/compactor-helpers.ts +135 -6
- package/src/elision-helpers.ts +14 -5
- package/src/elision-state.test.ts +78 -0
- package/src/elision-state.ts +42 -0
- package/src/index.ts +13 -19
- package/src/mode/project-messages.ts +13 -1
- package/src/package-root.test.ts +37 -0
- package/src/server.ts +34 -0
- package/src/transcriber.test.ts +12 -0
- package/src/transcriber.ts +14 -0
- package/src/tunnel.test.ts +21 -0
- package/src/tunnel.ts +10 -1
|
@@ -255,3 +255,81 @@ describe('computeElisionState (golden: fused == 4-pass reference)', () => {
|
|
|
255
255
|
assertSameState(state, computeElisionStateOld(events));
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
260
|
+
// Memo correctness (complexity-hotspots-7 / u122-2): the single-slot memo keyed
|
|
261
|
+
// on the input array's IDENTITY must (a) return the cached state for the same
|
|
262
|
+
// immutable snapshot (a cache HIT — the identical reference), and (b) RECOMPUTE
|
|
263
|
+
// — never serve a stale state — for any different array, including a new event
|
|
264
|
+
// appended past the HWM or a re-config that reuses the same ids/seqs.
|
|
265
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
266
|
+
describe('computeElisionState (memo correctness)', () => {
|
|
267
|
+
const baseLog = (): MoxxyEvent[] => [
|
|
268
|
+
event(0, { type: 'user_prompt', turnId: t1, source: 'user', text: 'the task' }),
|
|
269
|
+
event(1, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: asToolCallId('ra'), name: 'recall', input: { callId: 'x' } }),
|
|
270
|
+
event(2, { type: 'tool_result', turnId: t1, source: 'tool', callId: asToolCallId('ra'), ok: true, output: 'A'.repeat(300) }),
|
|
271
|
+
event(3, {
|
|
272
|
+
type: 'elision', turnId: t2, source: 'system', elidedThrough: 2, stubbedRanges: [[0, 2]],
|
|
273
|
+
elideConversational: true, conversationalRecallThreshold: 4, maxRecallBytes: 1000, neverElideTools: [], tokensSaved: 100,
|
|
274
|
+
}),
|
|
275
|
+
event(4, { type: 'user_prompt', turnId: t2, source: 'user', text: 'recent' }),
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
it('returns the cached state for the same snapshot, equal to a fresh fold', () => {
|
|
279
|
+
const events = baseLog();
|
|
280
|
+
const a = computeElisionState(events);
|
|
281
|
+
// Same array reference → memo HIT → the identical cached reference.
|
|
282
|
+
expect(computeElisionState(events)).toBe(a);
|
|
283
|
+
// …and it equals an uncached fresh fold of the same snapshot.
|
|
284
|
+
assertSameState(a, computeElisionStateOld(events));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('invalidates (recomputes) when a new event is appended past the HWM', () => {
|
|
288
|
+
const events = baseLog();
|
|
289
|
+
const before = computeElisionState(events);
|
|
290
|
+
expect(before.hwm).toBe(2);
|
|
291
|
+
// A NEW snapshot array with one more tail event must NOT serve `before`.
|
|
292
|
+
const grown = [
|
|
293
|
+
...events,
|
|
294
|
+
event(5, { type: 'tool_call_requested', turnId: t2, source: 'model', callId: asToolCallId('rb'), name: 'recall', input: { callId: 'y' } }),
|
|
295
|
+
];
|
|
296
|
+
const after = computeElisionState(grown);
|
|
297
|
+
expect(after).not.toBe(before); // recomputed, not the cached ref
|
|
298
|
+
// `rb` only exists in the grown log → its presence proves invalidation.
|
|
299
|
+
expect(after.recallResultCallIds.has('rb')).toBe(true);
|
|
300
|
+
expect(before.recallResultCallIds.has('rb')).toBe(false);
|
|
301
|
+
assertSameState(after, computeElisionStateOld(grown));
|
|
302
|
+
// A new ElisionEvent that advances the HWM also invalidates.
|
|
303
|
+
const reElided = [
|
|
304
|
+
...grown,
|
|
305
|
+
event(6, { type: 'tool_result', turnId: t2, source: 'tool', callId: asToolCallId('rb'), ok: true, output: 'B'.repeat(50) }),
|
|
306
|
+
event(7, {
|
|
307
|
+
type: 'elision', turnId: t2, source: 'system', elidedThrough: 6, stubbedRanges: [[3, 6]],
|
|
308
|
+
elideConversational: true, conversationalRecallThreshold: 4, maxRecallBytes: 1000, neverElideTools: [], tokensSaved: 200,
|
|
309
|
+
}),
|
|
310
|
+
];
|
|
311
|
+
const advanced = computeElisionState(reElided);
|
|
312
|
+
expect(advanced.hwm).toBe(6);
|
|
313
|
+
assertSameState(advanced, computeElisionStateOld(reElided));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('never serves a stale state for a re-config that reuses the same ids/seqs', () => {
|
|
317
|
+
// Two logically-distinct logs with byte-identical ids/seqs but different
|
|
318
|
+
// elision config — the exact case a content hash of id+seq would collide on
|
|
319
|
+
// and serve stale. Distinct array instances → distinct memo keys → correct.
|
|
320
|
+
const mk = (threshold: number): MoxxyEvent[] => [
|
|
321
|
+
event(0, { type: 'user_prompt', turnId: t1, source: 'user', text: 'the task' }),
|
|
322
|
+
event(1, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: asToolCallId('s1'), name: 'recall', input: { seq: 0 } }),
|
|
323
|
+
event(2, { type: 'tool_call_requested', turnId: t1, source: 'model', callId: asToolCallId('s2'), name: 'recall', input: { seq: 0 } }),
|
|
324
|
+
event(3, {
|
|
325
|
+
type: 'elision', turnId: t2, source: 'system', elidedThrough: 2, stubbedRanges: [[0, 2]],
|
|
326
|
+
elideConversational: true, conversationalRecallThreshold: threshold, maxRecallBytes: 1000, neverElideTools: [], tokensSaved: 10,
|
|
327
|
+
}),
|
|
328
|
+
event(4, { type: 'user_prompt', turnId: t2, source: 'user', text: 'next' }),
|
|
329
|
+
];
|
|
330
|
+
const on = computeElisionState(mk(3)); // threshold 3 > 2 seq-recalls → ON
|
|
331
|
+
const off = computeElisionState(mk(2)); // threshold 2 == 2 → OFF
|
|
332
|
+
expect(on.effectiveElideConversational).toBe(true);
|
|
333
|
+
expect(off.effectiveElideConversational).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
});
|
package/src/elision-state.ts
CHANGED
|
@@ -70,12 +70,54 @@ const EMPTY_STATE: ElisionState = {
|
|
|
70
70
|
firstUserPromptSeq: -1,
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Single-slot memo of the most recent {@link computeElisionState} result,
|
|
75
|
+
* keyed on the IDENTITY of the events array it was folded from.
|
|
76
|
+
*
|
|
77
|
+
* The event log is append-only and every held event is immutable (invariant
|
|
78
|
+
* #6: events at/below the HWM never change), so a given snapshot array is a
|
|
79
|
+
* stable, never-mutated value: the same array reference always denotes the
|
|
80
|
+
* same content and therefore the same state. A new turn produces a NEW snapshot
|
|
81
|
+
* array (the live `log.slice()` returns a fresh array each call), so the memo
|
|
82
|
+
* self-invalidates the moment the log changes — there is no way for it to serve
|
|
83
|
+
* a state that is stale for the array it is keyed on.
|
|
84
|
+
*
|
|
85
|
+
* Keying on identity (not a content hash of `id`/`seq`/payload) is the only
|
|
86
|
+
* SOUND single-slot choice for a pure fold over an arbitrary array: two
|
|
87
|
+
* logically-different logs can share ids/seqs (e.g. a test that rebuilds the
|
|
88
|
+
* same prefix with different payload, or a re-config), and a content hash that
|
|
89
|
+
* missed a payload field would serve a stale state — a correctness bug. Array
|
|
90
|
+
* identity can never collide across distinct values.
|
|
91
|
+
*
|
|
92
|
+
* The win: callers that re-ask over the SAME snapshot within an iteration
|
|
93
|
+
* (e.g. the elision/compaction gates and the estimate, when they thread the
|
|
94
|
+
* one `log.slice()` array — see `estimateContextTokens`) fold it only once;
|
|
95
|
+
* `WeakMap` would also help a held snapshot survive GC pressure, but a single
|
|
96
|
+
* slot keeps it allocation-free and matches the "one live snapshot at a time"
|
|
97
|
+
* access pattern. Threading a precomputed state is the explicit zero-cost fast
|
|
98
|
+
* path; this memo covers callers that re-pass the same array but can't thread.
|
|
99
|
+
*/
|
|
100
|
+
let memoEvents: ReadonlyArray<MoxxyEvent> | null = null;
|
|
101
|
+
let memoState: ElisionState | null = null;
|
|
102
|
+
|
|
73
103
|
/**
|
|
74
104
|
* Derive elision state purely from the log: the active high-water mark + flags
|
|
75
105
|
* (from the latest ElisionEvent), the callId→tool map, recall bookkeeping, the
|
|
76
106
|
* adaptive conversational auto-disable, and which pinned recalls exceed the cap.
|
|
107
|
+
*
|
|
108
|
+
* Memoized on the input array's identity (see above) so repeated calls over the
|
|
109
|
+
* same immutable snapshot fold it only once. The returned state is identical
|
|
110
|
+
* (and `===` on a cache hit) to a fresh fold.
|
|
77
111
|
*/
|
|
78
112
|
export function computeElisionState(events: ReadonlyArray<MoxxyEvent>): ElisionState {
|
|
113
|
+
if (memoEvents === events && memoState !== null) return memoState;
|
|
114
|
+
const state = computeElisionStateUncached(events);
|
|
115
|
+
memoEvents = events;
|
|
116
|
+
memoState = state;
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function computeElisionStateUncached(events: ReadonlyArray<MoxxyEvent>): ElisionState {
|
|
79
121
|
let hwm = -1;
|
|
80
122
|
let elideConversational = false;
|
|
81
123
|
let conversationalRecallThreshold = Number.POSITIVE_INFINITY;
|
package/src/index.ts
CHANGED
|
@@ -181,15 +181,12 @@ export type {
|
|
|
181
181
|
SpawnCliTunnelOptions,
|
|
182
182
|
CliTunnelHandle,
|
|
183
183
|
} from './tunnel.js';
|
|
184
|
-
|
|
184
|
+
// Node-runtime helpers (spawnCliTunnel/isCliTunnelAvailable, writeFileAtomic*,
|
|
185
|
+
// moxxyHome/moxxyPath, readRequestBody/bearerTokenMatches, channel-auth) are
|
|
186
|
+
// exported from the './server' subpath, NOT the main barrel — they statically
|
|
187
|
+
// reach node:* builtins and would break a browser/RN bundle. See ./server.ts.
|
|
185
188
|
export { isRetryableError, toFriendlyError, zodToJsonSchema, estimateTextTokens, type StopReason } from './provider-utils.js';
|
|
186
|
-
export {
|
|
187
|
-
writeFileAtomic,
|
|
188
|
-
writeFileAtomicSync,
|
|
189
|
-
moxxyHome,
|
|
190
|
-
moxxyPath,
|
|
191
|
-
type WriteFileAtomicOptions,
|
|
192
|
-
} from './fs-utils.js';
|
|
189
|
+
export type { WriteFileAtomicOptions } from './fs-utils.js';
|
|
193
190
|
export { createMutex, type Mutex } from './mutex.js';
|
|
194
191
|
export {
|
|
195
192
|
createJsonFileStore,
|
|
@@ -198,17 +195,9 @@ export {
|
|
|
198
195
|
} from './json-file-store.js';
|
|
199
196
|
export { assertNever } from './assert.js';
|
|
200
197
|
export { compareSemver, parseSemverCore } from './semver.js';
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
rotateChannelToken,
|
|
205
|
-
bearerGuard,
|
|
206
|
-
encodeWsBearerProtocol,
|
|
207
|
-
tokenFromWsProtocolHeader,
|
|
208
|
-
MOXXY_WS_SUBPROTOCOL,
|
|
209
|
-
MOXXY_WS_BEARER_PROTOCOL_PREFIX,
|
|
210
|
-
type ChannelTokenOptions,
|
|
211
|
-
} from './channel-auth.js';
|
|
198
|
+
// readRequestBody/bearerTokenMatches (http-utils) and the channel-auth value
|
|
199
|
+
// helpers live on the './server' subpath — they reach node:http/crypto/fs/path.
|
|
200
|
+
export type { ChannelTokenOptions } from './channel-auth.js';
|
|
212
201
|
export {
|
|
213
202
|
autoAllowResolver,
|
|
214
203
|
denyByDefaultResolver,
|
|
@@ -254,7 +243,11 @@ export type { TokenBudget, CompactContext, CompactorDef } from './compactor.js';
|
|
|
254
243
|
export {
|
|
255
244
|
estimateContextTokens,
|
|
256
245
|
runCompactionIfNeeded,
|
|
246
|
+
runManualCompaction,
|
|
247
|
+
resolveModelContext,
|
|
257
248
|
isContextOverflowError,
|
|
249
|
+
type ManualCompactionInput,
|
|
250
|
+
type ManualCompactionResult,
|
|
258
251
|
} from './compactor-helpers.js';
|
|
259
252
|
export {
|
|
260
253
|
runElisionIfNeeded,
|
|
@@ -403,6 +396,7 @@ export type {
|
|
|
403
396
|
TranscriptionSegment,
|
|
404
397
|
TranscribeOptions,
|
|
405
398
|
} from './transcriber.js';
|
|
399
|
+
export { MOXXY_PCM16_24KHZ_MIME } from './transcriber.js';
|
|
406
400
|
|
|
407
401
|
export type {
|
|
408
402
|
Synthesizer,
|
|
@@ -61,6 +61,15 @@ export interface ProjectMessagesOptions {
|
|
|
61
61
|
readonly systemPrompt?: string;
|
|
62
62
|
/** Optional trailing user message — useful for plan-execute's "Focus on this step now: X". */
|
|
63
63
|
readonly trailingUserText?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Optional precomputed elision state for THIS exact log snapshot. When a
|
|
66
|
+
* caller already derived it within the same loop iteration (e.g. the
|
|
67
|
+
* compaction/elision gates), threading it here skips a redundant
|
|
68
|
+
* `computeElisionState` fold. MUST be the state of the same log — a stale one
|
|
69
|
+
* would mis-render stubs — so it is purely an opt-in fast path; omitting it
|
|
70
|
+
* recomputes (memoized on the log version), byte-identically.
|
|
71
|
+
*/
|
|
72
|
+
readonly precomputedElisionState?: ElisionState;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
interface CompactionRange {
|
|
@@ -260,7 +269,10 @@ export function projectMessages(
|
|
|
260
269
|
const compactions = activeCompactionRanges(allEvents);
|
|
261
270
|
const compactionFor = makeCompactionLookup(compactions);
|
|
262
271
|
const emittedCompactions = new Set<CompactionRange>();
|
|
263
|
-
|
|
272
|
+
// Reuse a threaded state when the caller already derived it for this exact
|
|
273
|
+
// snapshot; otherwise `computeElisionState` is memoized on the log version so
|
|
274
|
+
// the in-iteration projection still folds only once.
|
|
275
|
+
const el = opts.precomputedElisionState ?? computeElisionState(allEvents);
|
|
264
276
|
|
|
265
277
|
const messages: ProviderMessage[] = [];
|
|
266
278
|
// The stable prefix is every message produced from events at/below the
|
package/src/package-root.test.ts
CHANGED
|
@@ -37,4 +37,41 @@ describe('@moxxy/sdk package root', () => {
|
|
|
37
37
|
|
|
38
38
|
expect(check.issues[0]?.requirement.name).toBe('auth:provider:openai-codex');
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
// The main barrel is the browser/RN-safe surface: a value import of any of
|
|
42
|
+
// these would drag a node:* builtin into a Metro/browser bundle. They live on
|
|
43
|
+
// the './server' subpath instead. This pins the split so a future accidental
|
|
44
|
+
// re-export on the main barrel is caught (see .dependency-cruiser.cjs
|
|
45
|
+
// `no-node-builtins-in-renderer`).
|
|
46
|
+
const NODE_ONLY_VALUE_EXPORTS = [
|
|
47
|
+
'spawnCliTunnel',
|
|
48
|
+
'isCliTunnelAvailable',
|
|
49
|
+
'writeFileAtomic',
|
|
50
|
+
'writeFileAtomicSync',
|
|
51
|
+
'moxxyHome',
|
|
52
|
+
'moxxyPath',
|
|
53
|
+
'readRequestBody',
|
|
54
|
+
'bearerTokenMatches',
|
|
55
|
+
'resolveChannelToken',
|
|
56
|
+
'rotateChannelToken',
|
|
57
|
+
'bearerGuard',
|
|
58
|
+
'encodeWsBearerProtocol',
|
|
59
|
+
'tokenFromWsProtocolHeader',
|
|
60
|
+
'MOXXY_WS_SUBPROTOCOL',
|
|
61
|
+
'MOXXY_WS_BEARER_PROTOCOL_PREFIX',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
it('does NOT expose Node-runtime values on the browser/RN-safe main barrel', async () => {
|
|
65
|
+
const sdk: Record<string, unknown> = await import('@moxxy/sdk');
|
|
66
|
+
for (const name of NODE_ONLY_VALUE_EXPORTS) {
|
|
67
|
+
expect(sdk[name], `${name} must not be on the @moxxy/sdk main barrel`).toBeUndefined();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('exposes the Node-runtime helpers on the @moxxy/sdk/server subpath', async () => {
|
|
72
|
+
const server: Record<string, unknown> = await import('@moxxy/sdk/server');
|
|
73
|
+
for (const name of NODE_ONLY_VALUE_EXPORTS) {
|
|
74
|
+
expect(server[name], `${name} must be exported from @moxxy/sdk/server`).toBeDefined();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
40
77
|
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-runtime-only surface of @moxxy/sdk.
|
|
3
|
+
*
|
|
4
|
+
* Importing `@moxxy/sdk/server` pulls in the value helpers that statically
|
|
5
|
+
* depend on Node builtins (`node:child_process`, `node:fs`, `node:os`,
|
|
6
|
+
* `node:http`, `node:crypto`, `node:path`). The MAIN barrel (`@moxxy/sdk`) is
|
|
7
|
+
* deliberately kept free of these so a browser/React-Native bundle can value-
|
|
8
|
+
* import from it (and from `@moxxy/sdk/tool-display`) without dragging a Node
|
|
9
|
+
* builtin into the bundle — Metro cannot polyfill `node:child_process`.
|
|
10
|
+
*
|
|
11
|
+
* Node-side consumers (cli, runner, desktop-host, channel/oauth/webhooks
|
|
12
|
+
* plugins, …) import these helpers from here. The corresponding *type* exports
|
|
13
|
+
* (e.g. `TunnelHandle`, `WriteFileAtomicOptions`, `ChannelTokenOptions`) remain
|
|
14
|
+
* on the main barrel because types are erased at build time and never reach a
|
|
15
|
+
* bundle.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export { spawnCliTunnel, isCliTunnelAvailable } from './tunnel.js';
|
|
19
|
+
export {
|
|
20
|
+
writeFileAtomic,
|
|
21
|
+
writeFileAtomicSync,
|
|
22
|
+
moxxyHome,
|
|
23
|
+
moxxyPath,
|
|
24
|
+
} from './fs-utils.js';
|
|
25
|
+
export { readRequestBody, bearerTokenMatches } from './http-utils.js';
|
|
26
|
+
export {
|
|
27
|
+
resolveChannelToken,
|
|
28
|
+
rotateChannelToken,
|
|
29
|
+
bearerGuard,
|
|
30
|
+
encodeWsBearerProtocol,
|
|
31
|
+
tokenFromWsProtocolHeader,
|
|
32
|
+
MOXXY_WS_SUBPROTOCOL,
|
|
33
|
+
MOXXY_WS_BEARER_PROTOCOL_PREFIX,
|
|
34
|
+
} from './channel-auth.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { MOXXY_PCM16_24KHZ_MIME } from './transcriber.js';
|
|
3
|
+
|
|
4
|
+
describe('MOXXY_PCM16_24KHZ_MIME', () => {
|
|
5
|
+
it('locks the cross-package wire value', () => {
|
|
6
|
+
// This is a PROTOCOL constant: the desktop/web capture path stamps it onto
|
|
7
|
+
// the audio blob and the Codex transcriber keys on it to wrap the raw PCM
|
|
8
|
+
// samples in a WAV header. Drift in the literal silently breaks
|
|
9
|
+
// transcription with no compile-time signal — so pin the exact bytes here.
|
|
10
|
+
expect(MOXXY_PCM16_24KHZ_MIME).toBe('audio/x-moxxy-pcm16-24khz');
|
|
11
|
+
});
|
|
12
|
+
});
|
package/src/transcriber.ts
CHANGED
|
@@ -18,6 +18,20 @@
|
|
|
18
18
|
* used by models that advertise `supportsAudio: true`.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Wire MIME tag for raw 16-bit little-endian PCM mono at 24 kHz — the lossless
|
|
23
|
+
* format the desktop/web mic capture path produces and that the Codex
|
|
24
|
+
* transcriber keys on to wrap the bytes in a WAV header before upload.
|
|
25
|
+
*
|
|
26
|
+
* This is a cross-package PROTOCOL value: client-platform-web stamps it onto
|
|
27
|
+
* the captured blob, plugin-cli forwards it as the attachment `mimeType`, and
|
|
28
|
+
* plugin-stt-whisper switches on it. It lived as an independently-redeclared
|
|
29
|
+
* literal in all three (silent transcription breakage if any drifted), so it is
|
|
30
|
+
* hoisted here to the SDK's single zero-dep typed surface as the source of
|
|
31
|
+
* truth. Consumers should import this rather than re-spell the literal.
|
|
32
|
+
*/
|
|
33
|
+
export const MOXXY_PCM16_24KHZ_MIME = 'audio/x-moxxy-pcm16-24khz';
|
|
34
|
+
|
|
21
35
|
export interface TranscriptionSegment {
|
|
22
36
|
/** Segment start, in seconds from the start of the clip. */
|
|
23
37
|
readonly start: number;
|
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
|
};
|