mixdog 0.9.52 → 0.9.53
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/package.json +1 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
4
8
|
|
|
5
9
|
import {
|
|
6
10
|
OpenAICompatProvider,
|
|
@@ -10,6 +14,7 @@ import {
|
|
|
10
14
|
} from '../src/runtime/agent/orchestrator/providers/openai-compat.mjs';
|
|
11
15
|
import { consumeCompatChatCompletionStream } from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
12
16
|
import { GrokOAuthProvider } from '../src/runtime/agent/orchestrator/providers/grok-oauth.mjs';
|
|
17
|
+
import { sendViaWebSocket } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
13
18
|
import {
|
|
14
19
|
OpenCodeGoProvider,
|
|
15
20
|
isAnthropicGoModel,
|
|
@@ -17,6 +22,11 @@ import {
|
|
|
17
22
|
resolveOpenCodeGoBaseURLs,
|
|
18
23
|
} from '../src/runtime/agent/orchestrator/providers/opencode-go.mjs';
|
|
19
24
|
import { uncachedInputTokensForProvider } from '../src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs';
|
|
25
|
+
import { resolveTraceUsageInput } from '../src/runtime/agent/orchestrator/agent-trace.mjs';
|
|
26
|
+
import {
|
|
27
|
+
billableInputTokensForProvider,
|
|
28
|
+
isInclusiveProvider,
|
|
29
|
+
} from '../src/runtime/shared/llm/cost.mjs';
|
|
20
30
|
|
|
21
31
|
function stream(events) {
|
|
22
32
|
return {
|
|
@@ -173,17 +183,143 @@ test('OpenCode Go normalizes both route families to inclusive provider usage', a
|
|
|
173
183
|
provider.openai = { send: async () => openaiRaw };
|
|
174
184
|
const anthropic = await provider.send([], 'minimax-m3', [], {});
|
|
175
185
|
assert.equal(OpenCodeGoProvider.inputExcludesCache, false);
|
|
186
|
+
assert.equal(isInclusiveProvider('opencode-go'), true);
|
|
176
187
|
assert.equal(anthropic.usage.inputTokens, 100);
|
|
177
188
|
assert.equal(anthropic.usage.promptTokens, 100);
|
|
178
189
|
assert.equal(uncachedInputTokensForProvider('opencode-go', anthropic.usage.inputTokens, 35, 5), 60);
|
|
190
|
+
assert.equal(billableInputTokensForProvider('opencode-go', 100, 35, 5), 60);
|
|
191
|
+
assert.deepEqual(resolveTraceUsageInput({
|
|
192
|
+
provider: 'opencode-go',
|
|
193
|
+
inputTokens: 60,
|
|
194
|
+
cachedTokens: 35,
|
|
195
|
+
cacheWriteTokens: 5,
|
|
196
|
+
inputTokensInclusive: false,
|
|
197
|
+
}), {
|
|
198
|
+
uncachedInputTokens: 60,
|
|
199
|
+
promptTokens: 100,
|
|
200
|
+
});
|
|
179
201
|
assert.equal(anthropic.usage.inputTokens, 100, 'context footprint is inclusive');
|
|
180
202
|
|
|
181
203
|
const openai = await provider.send([], 'glm-5.2', [], {});
|
|
182
204
|
assert.equal(openai, openaiRaw);
|
|
183
205
|
assert.equal(uncachedInputTokensForProvider('opencode-go', openai.usage.inputTokens, 40, 0), 60);
|
|
206
|
+
assert.equal(billableInputTokensForProvider('opencode-go', 100, 40, 0), 60);
|
|
207
|
+
assert.deepEqual(resolveTraceUsageInput({
|
|
208
|
+
provider: 'opencode-go',
|
|
209
|
+
inputTokens: 100,
|
|
210
|
+
cachedTokens: 40,
|
|
211
|
+
cacheWriteTokens: 0,
|
|
212
|
+
}), {
|
|
213
|
+
uncachedInputTokens: 60,
|
|
214
|
+
promptTokens: 100,
|
|
215
|
+
});
|
|
184
216
|
assert.equal(openai.usage.inputTokens, 100, 'context footprint remains inclusive');
|
|
185
217
|
});
|
|
186
218
|
|
|
219
|
+
test('OpenCode Go Anthropic delegation traces additive inner usage before inclusive outer normalization', (t) => {
|
|
220
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'mixdog-opencode-go-trace-'));
|
|
221
|
+
const tracePath = join(tempDir, 'agent-trace.jsonl');
|
|
222
|
+
t.after(() => rmSync(tempDir, { recursive: true, force: true }));
|
|
223
|
+
|
|
224
|
+
const openCodeGoUrl = new URL('../src/runtime/agent/orchestrator/providers/opencode-go.mjs', import.meta.url).href;
|
|
225
|
+
const anthropicUrl = new URL('../src/runtime/agent/orchestrator/providers/anthropic.mjs', import.meta.url).href;
|
|
226
|
+
const traceUrl = new URL('../src/runtime/agent/orchestrator/agent-trace.mjs', import.meta.url).href;
|
|
227
|
+
const fixture = `
|
|
228
|
+
const { OpenCodeGoProvider } = await import(${JSON.stringify(openCodeGoUrl)});
|
|
229
|
+
const { AnthropicProvider } = await import(${JSON.stringify(anthropicUrl)});
|
|
230
|
+
const { drainAgentTrace } = await import(${JSON.stringify(traceUrl)});
|
|
231
|
+
|
|
232
|
+
const encoder = new TextEncoder();
|
|
233
|
+
const events = [
|
|
234
|
+
{ type: 'message_start', message: {
|
|
235
|
+
model: 'minimax-m3',
|
|
236
|
+
usage: {
|
|
237
|
+
input_tokens: 60,
|
|
238
|
+
cache_read_input_tokens: 35,
|
|
239
|
+
cache_creation_input_tokens: 5,
|
|
240
|
+
},
|
|
241
|
+
} },
|
|
242
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 5 } },
|
|
243
|
+
{ type: 'message_stop' },
|
|
244
|
+
];
|
|
245
|
+
const chunks = events.map((event) => encoder.encode(
|
|
246
|
+
'event: ' + event.type + '\\ndata: ' + JSON.stringify(event) + '\\n\\n'
|
|
247
|
+
));
|
|
248
|
+
let chunkIndex = 0;
|
|
249
|
+
const response = {
|
|
250
|
+
ok: true,
|
|
251
|
+
status: 200,
|
|
252
|
+
headers: new Map(),
|
|
253
|
+
body: { getReader() {
|
|
254
|
+
return {
|
|
255
|
+
read() {
|
|
256
|
+
return chunkIndex < chunks.length
|
|
257
|
+
? Promise.resolve({ done: false, value: chunks[chunkIndex++] })
|
|
258
|
+
: Promise.resolve({ done: true, value: undefined });
|
|
259
|
+
},
|
|
260
|
+
cancel() { return Promise.resolve(); },
|
|
261
|
+
releaseLock() {},
|
|
262
|
+
};
|
|
263
|
+
} },
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const inner = Object.create(AnthropicProvider.prototype);
|
|
267
|
+
inner.name = 'opencode-go';
|
|
268
|
+
inner.config = { disableBetaHeaders: true };
|
|
269
|
+
inner.fastModeBetaHeaderLatched = false;
|
|
270
|
+
inner.client = { messages: { create() {
|
|
271
|
+
return { asResponse: async () => response };
|
|
272
|
+
} } };
|
|
273
|
+
|
|
274
|
+
const outer = Object.create(OpenCodeGoProvider.prototype);
|
|
275
|
+
outer.anthropic = inner;
|
|
276
|
+
outer.openai = { send() { throw new Error('wrong OpenCode Go route'); } };
|
|
277
|
+
const result = await outer.send(
|
|
278
|
+
[{ role: 'user', content: 'fixture' }],
|
|
279
|
+
'minimax-m3',
|
|
280
|
+
[],
|
|
281
|
+
{ sessionId: 'opencode-go-additive-trace' },
|
|
282
|
+
);
|
|
283
|
+
await drainAgentTrace();
|
|
284
|
+
process.stdout.write(JSON.stringify(result.usage));
|
|
285
|
+
`;
|
|
286
|
+
const child = spawnSync(process.execPath, ['--input-type=module', '-e', fixture], {
|
|
287
|
+
cwd: new URL('..', import.meta.url),
|
|
288
|
+
encoding: 'utf8',
|
|
289
|
+
env: {
|
|
290
|
+
...process.env,
|
|
291
|
+
MIXDOG_AGENT_TRACE_DISABLE: '0',
|
|
292
|
+
MIXDOG_AGENT_TRACE_LOCAL_DISABLE: '0',
|
|
293
|
+
MIXDOG_AGENT_TRACE_PATH: tracePath,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
297
|
+
|
|
298
|
+
const outerUsage = JSON.parse(child.stdout);
|
|
299
|
+
assert.deepEqual(outerUsage, {
|
|
300
|
+
inputTokens: 100,
|
|
301
|
+
outputTokens: 5,
|
|
302
|
+
cachedTokens: 35,
|
|
303
|
+
cacheWriteTokens: 5,
|
|
304
|
+
promptTokens: 100,
|
|
305
|
+
});
|
|
306
|
+
const traceRows = readFileSync(tracePath, 'utf8')
|
|
307
|
+
.trim()
|
|
308
|
+
.split(/\r?\n/)
|
|
309
|
+
.map((line) => JSON.parse(line));
|
|
310
|
+
const innerUsage = traceRows.find((row) => (
|
|
311
|
+
row.kind === 'usage_raw'
|
|
312
|
+
&& row.session_id === 'opencode-go-additive-trace'
|
|
313
|
+
));
|
|
314
|
+
assert.ok(innerUsage, 'delegated Anthropic route must emit usage_raw');
|
|
315
|
+
assert.equal(innerUsage.payload.provider, 'opencode-go');
|
|
316
|
+
assert.equal(innerUsage.input_tokens, 60);
|
|
317
|
+
assert.equal(innerUsage.cached_tokens, 35);
|
|
318
|
+
assert.equal(innerUsage.cache_write_tokens, 5);
|
|
319
|
+
assert.equal(innerUsage.uncached_input_tokens, 60);
|
|
320
|
+
assert.equal(innerUsage.prompt_tokens, 100);
|
|
321
|
+
});
|
|
322
|
+
|
|
187
323
|
test('constructing Grok OAuth is network inert', () => {
|
|
188
324
|
const provider = new GrokOAuthProvider({
|
|
189
325
|
preconnectFn: () => { throw new Error('constructor attempted outbound preconnect'); },
|
|
@@ -266,3 +402,124 @@ test('Grok OAuth does not refresh/replay a 401 after visible tool dispatch', asy
|
|
|
266
402
|
assert.equal(dispatched, 1);
|
|
267
403
|
assert.equal(authCalls, 1);
|
|
268
404
|
});
|
|
405
|
+
|
|
406
|
+
test('xAI WS to HTTP fallback preserves completed warmup usage and cost ticks', async (t) => {
|
|
407
|
+
const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
408
|
+
process.env.MIXDOG_OAI_TRANSPORT = 'auto';
|
|
409
|
+
t.after(() => {
|
|
410
|
+
if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
411
|
+
else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
|
|
412
|
+
});
|
|
413
|
+
const provider = new OpenAICompatProvider('xai', {
|
|
414
|
+
apiKey: 'fixture',
|
|
415
|
+
preconnect: false,
|
|
416
|
+
responsesTransport: 'websocket',
|
|
417
|
+
});
|
|
418
|
+
const warmup = {
|
|
419
|
+
requestBody: { generate: false },
|
|
420
|
+
usage: { inputTokens: 10, outputTokens: 1, cachedTokens: 4, promptTokens: 10, raw: { cost_in_usd_ticks: 100 } },
|
|
421
|
+
};
|
|
422
|
+
provider._doSendXaiResponsesWebSocket = async () => {
|
|
423
|
+
const err = Object.assign(new Error('upgrade required'), { httpStatus: 426 });
|
|
424
|
+
Object.defineProperty(err, '__warmup', { value: warmup });
|
|
425
|
+
throw err;
|
|
426
|
+
};
|
|
427
|
+
provider._doSendXaiResponses = async () => ({
|
|
428
|
+
content: 'fallback',
|
|
429
|
+
usage: { inputTokens: 20, outputTokens: 2, cachedTokens: 5, promptTokens: 20, raw: { cost_in_usd_ticks: 200 }, costUsd: 0.00000002 },
|
|
430
|
+
});
|
|
431
|
+
const result = await provider._doSend([], 'grok-4.5', [], {});
|
|
432
|
+
assert.equal(result.usage.inputTokens, 30);
|
|
433
|
+
assert.equal(result.usage.mainInputTokens, 20);
|
|
434
|
+
assert.equal(result.usage.raw.cost_in_usd_ticks, 300);
|
|
435
|
+
assert.equal(result.usage.costUsd, 0.00000003);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('xAI WS warmup billing exposes only main request usage as context', async () => {
|
|
439
|
+
let streams = 0;
|
|
440
|
+
const result = await sendViaWebSocket({
|
|
441
|
+
auth: { type: 'xai', apiKey: 'fixture' },
|
|
442
|
+
body: { model: 'grok-4.5', input: [{ role: 'user', content: 'main' }] },
|
|
443
|
+
sendOpts: {},
|
|
444
|
+
externalSignal: null,
|
|
445
|
+
poolKey: 'xai-warmup-accounting',
|
|
446
|
+
cacheKey: 'xai-warmup-accounting',
|
|
447
|
+
iteration: 1,
|
|
448
|
+
useModel: 'grok-4.5',
|
|
449
|
+
traceProvider: 'xai',
|
|
450
|
+
includeResponseId: true,
|
|
451
|
+
warmupBody: { model: 'grok-4.5', input: [], generate: false },
|
|
452
|
+
_acquireWithRetryFn: async () => ({ entry: { socket: { close() {} } }, reused: false }),
|
|
453
|
+
_sendFrameFn: async () => {},
|
|
454
|
+
_streamFn: async ({ state }) => {
|
|
455
|
+
streams += 1;
|
|
456
|
+
return state.warmup
|
|
457
|
+
? {
|
|
458
|
+
content: '',
|
|
459
|
+
model: 'grok-4.5',
|
|
460
|
+
toolCalls: [],
|
|
461
|
+
usage: { inputTokens: 10, outputTokens: 1, cachedTokens: 4, promptTokens: 10, raw: { cost_in_usd_ticks: 100 } },
|
|
462
|
+
responseId: 'warm',
|
|
463
|
+
responseItems: [],
|
|
464
|
+
}
|
|
465
|
+
: {
|
|
466
|
+
content: 'done',
|
|
467
|
+
model: 'grok-4.5',
|
|
468
|
+
toolCalls: [],
|
|
469
|
+
usage: { inputTokens: 20, outputTokens: 2, cachedTokens: 5, promptTokens: 20, raw: { cost_in_usd_ticks: 200 } },
|
|
470
|
+
responseId: 'main',
|
|
471
|
+
responseItems: [],
|
|
472
|
+
closeSocket: true,
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
_agentTraceFn: () => {},
|
|
476
|
+
_sendSpanTraceFn: () => {},
|
|
477
|
+
});
|
|
478
|
+
assert.equal(streams, 2);
|
|
479
|
+
assert.equal(result.usage.inputTokens, 30);
|
|
480
|
+
assert.equal(result.usage.mainInputTokens, 20);
|
|
481
|
+
assert.equal(result.usage.raw.cost_in_usd_ticks, 300);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test('xAI safe 401 replay carries completed warmup into the retry', async () => {
|
|
485
|
+
const provider = new OpenAICompatProvider('xai', { apiKey: 'fixture', preconnect: false });
|
|
486
|
+
const warmup = { usage: { inputTokens: 10 } };
|
|
487
|
+
let attempts = 0;
|
|
488
|
+
provider.reloadApiKey = () => {};
|
|
489
|
+
provider._doSend = async (_messages, _model, _tools, opts) => {
|
|
490
|
+
attempts += 1;
|
|
491
|
+
if (attempts === 1) {
|
|
492
|
+
const err = Object.assign(new Error('401'), { httpStatus: 401 });
|
|
493
|
+
Object.defineProperty(err, '__warmup', { value: warmup });
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
assert.equal(opts._carriedWarmup, warmup);
|
|
497
|
+
return { content: 'retried' };
|
|
498
|
+
};
|
|
499
|
+
assert.equal((await provider.send([], 'grok-4.5', [], {})).content, 'retried');
|
|
500
|
+
assert.equal(attempts, 2);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('Grok OAuth safe 401 replay carries completed warmup to refreshed xAI inner', async () => {
|
|
504
|
+
const provider = Object.create(GrokOAuthProvider.prototype);
|
|
505
|
+
provider.config = { preconnect: false };
|
|
506
|
+
const warmup = { usage: { inputTokens: 10 } };
|
|
507
|
+
let authCalls = 0;
|
|
508
|
+
provider.ensureAuth = async ({ forceRefresh = false } = {}) => {
|
|
509
|
+
authCalls += 1;
|
|
510
|
+
return { access_token: forceRefresh ? 'fresh' : 'stale' };
|
|
511
|
+
};
|
|
512
|
+
provider._ensureInner = (token) => ({
|
|
513
|
+
_doSend: async (_messages, _model, _tools, opts) => {
|
|
514
|
+
if (token === 'stale') {
|
|
515
|
+
const err = Object.assign(new Error('401'), { httpStatus: 401 });
|
|
516
|
+
Object.defineProperty(err, '__warmup', { value: warmup });
|
|
517
|
+
throw err;
|
|
518
|
+
}
|
|
519
|
+
assert.equal(opts._carriedWarmup, warmup);
|
|
520
|
+
return { content: 'retried' };
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
assert.equal((await provider.send([], 'grok-4.5', [], {})).content, 'retried');
|
|
524
|
+
assert.equal(authCalls, 2);
|
|
525
|
+
});
|
|
@@ -71,7 +71,10 @@ import {
|
|
|
71
71
|
import { buildAnthropicBetaHeaders } from '../src/runtime/agent/orchestrator/providers/anthropic-betas.mjs';
|
|
72
72
|
import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
|
|
73
73
|
import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
|
|
74
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
OpenAIOAuthProvider,
|
|
76
|
+
buildRequestBody as buildOpenAIOAuthRequestBody,
|
|
77
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
75
78
|
import { _convertMessagesToResponsesInputForTest } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
76
79
|
import { OpenAIDirectProvider } from '../src/runtime/agent/orchestrator/providers/openai-ws.mjs';
|
|
77
80
|
|
|
@@ -1053,6 +1056,46 @@ test('gemini cache usage: official cached token fields are subsets of prompt tok
|
|
|
1053
1056
|
assert.notEqual(sdkAlias.inputTokens, 0, 'snake_case SDK aliases must remain visible to provider return usage');
|
|
1054
1057
|
});
|
|
1055
1058
|
|
|
1059
|
+
test('gemini cache usage: total fallback excludes candidate and thought output tokens', () => {
|
|
1060
|
+
const camelCase = _resolveGeminiCacheUsage({
|
|
1061
|
+
usageMetadata: {
|
|
1062
|
+
totalTokenCount: 1000,
|
|
1063
|
+
candidatesTokenCount: 150,
|
|
1064
|
+
thoughtsTokenCount: 50,
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
assert.equal(camelCase.inputTokens, 800);
|
|
1068
|
+
|
|
1069
|
+
const snakeCase = _resolveGeminiCacheUsage({
|
|
1070
|
+
usageMetadata: {
|
|
1071
|
+
total_token_count: 1200,
|
|
1072
|
+
candidates_token_count: 175,
|
|
1073
|
+
thoughts_token_count: 25,
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
assert.equal(snakeCase.inputTokens, 1000);
|
|
1077
|
+
|
|
1078
|
+
const promptWins = _resolveGeminiCacheUsage({
|
|
1079
|
+
usageMetadata: {
|
|
1080
|
+
promptTokenCount: 900,
|
|
1081
|
+
totalTokenCount: 1200,
|
|
1082
|
+
candidatesTokenCount: 250,
|
|
1083
|
+
thoughtsTokenCount: 50,
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
assert.equal(promptWins.inputTokens, 900);
|
|
1087
|
+
|
|
1088
|
+
const explicitZero = _resolveGeminiCacheUsage({
|
|
1089
|
+
usageMetadata: {
|
|
1090
|
+
prompt_token_count: 0,
|
|
1091
|
+
total_token_count: 1200,
|
|
1092
|
+
candidates_token_count: 175,
|
|
1093
|
+
thoughts_token_count: 25,
|
|
1094
|
+
},
|
|
1095
|
+
});
|
|
1096
|
+
assert.equal(explicitZero.inputTokens, 0);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1056
1099
|
test('gemini cache usage: clamps over-reported cache and falls back only for attached cachedContent', () => {
|
|
1057
1100
|
const clamped = _resolveGeminiCacheUsage({
|
|
1058
1101
|
usageMetadata: { promptTokenCount: 100, cachedContentTokenCount: 150 },
|
|
@@ -2237,21 +2280,28 @@ test('openai oauth ws delta: warmup generate:false does not force request_proper
|
|
|
2237
2280
|
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2238
2281
|
try {
|
|
2239
2282
|
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2240
|
-
const warmupBody = { model: 'gpt-5.5', generate: false, input: [
|
|
2241
|
-
const body = { model: 'gpt-5.5', input: [
|
|
2283
|
+
const warmupBody = { model: 'gpt-5.5', generate: false, input: [] };
|
|
2284
|
+
const body = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'first real input' }] };
|
|
2242
2285
|
const entry = {
|
|
2243
|
-
lastRequestSansInput: _stableStringify(_sansInput(warmupBody)),
|
|
2286
|
+
lastRequestSansInput: _stableStringify(_sansInput(warmupBody, { normalizeWarmupGenerate: true })),
|
|
2244
2287
|
lastResponseId: 'resp_warm',
|
|
2245
|
-
lastRequestInput: [
|
|
2288
|
+
lastRequestInput: [],
|
|
2246
2289
|
lastResponseItems: [],
|
|
2247
2290
|
};
|
|
2248
|
-
assert.equal(_sansInput(warmupBody).generate, undefined);
|
|
2249
|
-
const delta = _computeDelta({ entry, body });
|
|
2291
|
+
assert.equal(_sansInput(warmupBody, { normalizeWarmupGenerate: true }).generate, undefined);
|
|
2292
|
+
const delta = _computeDelta({ entry, body, traceProvider: 'openai-oauth' });
|
|
2250
2293
|
assert.equal(delta.mode, 'delta');
|
|
2251
|
-
assert.
|
|
2294
|
+
assert.equal(delta.frame.previous_response_id, 'resp_warm');
|
|
2295
|
+
assert.deepEqual(delta.frame.input, body.input);
|
|
2252
2296
|
// Non-warmup generate difference still breaks the delta.
|
|
2253
|
-
const genEntry = {
|
|
2254
|
-
|
|
2297
|
+
const genEntry = {
|
|
2298
|
+
...entry,
|
|
2299
|
+
lastRequestSansInput: _stableStringify(_sansInput(
|
|
2300
|
+
{ ...warmupBody, generate: true },
|
|
2301
|
+
{ normalizeWarmupGenerate: true },
|
|
2302
|
+
)),
|
|
2303
|
+
};
|
|
2304
|
+
const genChanged = _computeDelta({ entry: genEntry, body, traceProvider: 'openai-oauth' });
|
|
2255
2305
|
assert.equal(genChanged.mode, 'full');
|
|
2256
2306
|
assert.equal(genChanged.reason, 'request_properties_changed');
|
|
2257
2307
|
} finally {
|
|
@@ -2260,6 +2310,118 @@ test('openai oauth ws delta: warmup generate:false does not force request_proper
|
|
|
2260
2310
|
}
|
|
2261
2311
|
});
|
|
2262
2312
|
|
|
2313
|
+
test('xAI keeps generate:false in the property guard and falls back to a full frame', () => {
|
|
2314
|
+
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2315
|
+
try {
|
|
2316
|
+
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
2317
|
+
const warmupBody = { model: 'grok-4', generate: false, input: [] };
|
|
2318
|
+
const body = { model: 'grok-4', input: [{ role: 'user', content: 'real request' }] };
|
|
2319
|
+
const entry = {
|
|
2320
|
+
lastRequestSansInput: _stableStringify(_sansInput(warmupBody)),
|
|
2321
|
+
lastResponseId: 'xai-warm',
|
|
2322
|
+
lastRequestInput: [],
|
|
2323
|
+
lastResponseItems: [],
|
|
2324
|
+
};
|
|
2325
|
+
assert.equal(_sansInput(warmupBody).generate, false);
|
|
2326
|
+
const delta = _computeDelta({ entry, body, traceProvider: 'xai' });
|
|
2327
|
+
assert.equal(delta.mode, 'full');
|
|
2328
|
+
assert.equal(delta.reason, 'request_properties_changed');
|
|
2329
|
+
assert.equal(delta.frame.previous_response_id, undefined);
|
|
2330
|
+
assert.deepEqual(delta.frame.input, body.input);
|
|
2331
|
+
} finally {
|
|
2332
|
+
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
2333
|
+
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
|
|
2337
|
+
test('OpenAI OAuth startup prewarm sends no transcript and anchors the first real WS request', async () => {
|
|
2338
|
+
const savedEnv = Object.fromEntries([
|
|
2339
|
+
'MIXDOG_OAI_TRANSPORT',
|
|
2340
|
+
'MIXDOG_OPENAI_OAUTH_WS_WARMUP',
|
|
2341
|
+
'MIXDOG_AGENT_TRACE_DISABLE',
|
|
2342
|
+
].map((name) => [name, process.env[name]]));
|
|
2343
|
+
Object.assign(process.env, {
|
|
2344
|
+
MIXDOG_OAI_TRANSPORT: 'ws-delta',
|
|
2345
|
+
MIXDOG_OPENAI_OAUTH_WS_WARMUP: '1',
|
|
2346
|
+
MIXDOG_AGENT_TRACE_DISABLE: '1',
|
|
2347
|
+
});
|
|
2348
|
+
try {
|
|
2349
|
+
const provider = new OpenAIOAuthProvider({});
|
|
2350
|
+
provider.ensureAuth = async () => ({ access_token: 'test-token' });
|
|
2351
|
+
const liveInput = [{
|
|
2352
|
+
type: 'message',
|
|
2353
|
+
role: 'user',
|
|
2354
|
+
content: [{ type: 'input_text', text: 'live-transcript-must-appear-once' }],
|
|
2355
|
+
}];
|
|
2356
|
+
const body = {
|
|
2357
|
+
model: 'gpt-5.5',
|
|
2358
|
+
instructions: 'base instructions',
|
|
2359
|
+
input: liveInput,
|
|
2360
|
+
tools: [{ type: 'function', name: 'read', parameters: { type: 'object' } }],
|
|
2361
|
+
stream: true,
|
|
2362
|
+
background: false,
|
|
2363
|
+
prompt_cache_key: 'prewarm-parity-test',
|
|
2364
|
+
};
|
|
2365
|
+
const frames = [];
|
|
2366
|
+
let streams = 0;
|
|
2367
|
+
const result = await provider.send([], 'gpt-5.5', [], {
|
|
2368
|
+
sessionId: 'prewarm-parity-test',
|
|
2369
|
+
_prebuiltBody: body,
|
|
2370
|
+
_sendViaWebSocketFn: (args) => sendViaWebSocket({
|
|
2371
|
+
...args,
|
|
2372
|
+
_acquireWithRetryFn: async () => ({ entry: { socket: { close() {} } }, reused: false }),
|
|
2373
|
+
_sendFrameFn: async (_entry, frame) => frames.push(JSON.parse(JSON.stringify(frame))),
|
|
2374
|
+
_streamFn: async ({ state }) => {
|
|
2375
|
+
streams += 1;
|
|
2376
|
+
return state.warmup
|
|
2377
|
+
? {
|
|
2378
|
+
content: '',
|
|
2379
|
+
model: 'gpt-5.5',
|
|
2380
|
+
toolCalls: [],
|
|
2381
|
+
usage: { inputTokens: 10, outputTokens: 0, cachedTokens: 0, promptTokens: 10 },
|
|
2382
|
+
responseId: 'warm-1',
|
|
2383
|
+
responseItems: [],
|
|
2384
|
+
}
|
|
2385
|
+
: {
|
|
2386
|
+
content: 'done',
|
|
2387
|
+
model: 'gpt-5.5',
|
|
2388
|
+
toolCalls: [],
|
|
2389
|
+
usage: { inputTokens: 20, outputTokens: 1, cachedTokens: 10, promptTokens: 20 },
|
|
2390
|
+
responseId: 'resp-1',
|
|
2391
|
+
responseItems: [],
|
|
2392
|
+
closeSocket: true,
|
|
2393
|
+
};
|
|
2394
|
+
},
|
|
2395
|
+
_agentTraceFn: () => {},
|
|
2396
|
+
_sendSpanTraceFn: () => {},
|
|
2397
|
+
}),
|
|
2398
|
+
});
|
|
2399
|
+
|
|
2400
|
+
assert.equal(streams, 2);
|
|
2401
|
+
assert.equal(frames.length, 2);
|
|
2402
|
+
const [warmup, followUp] = frames;
|
|
2403
|
+
assert.equal(warmup.type, 'response.create');
|
|
2404
|
+
assert.equal(warmup.generate, false);
|
|
2405
|
+
assert.deepEqual(warmup.input, []);
|
|
2406
|
+
assert.equal(warmup.instructions, body.instructions);
|
|
2407
|
+
assert.deepEqual(warmup.tools, body.tools);
|
|
2408
|
+
assert.equal(warmup.stream, undefined);
|
|
2409
|
+
assert.equal(warmup.background, undefined);
|
|
2410
|
+
assert.equal(JSON.stringify(warmup).includes('live-transcript-must-appear-once'), false);
|
|
2411
|
+
assert.equal(followUp.type, 'response.create');
|
|
2412
|
+
assert.equal(followUp.previous_response_id, 'warm-1');
|
|
2413
|
+
assert.deepEqual(followUp.input, liveInput);
|
|
2414
|
+
assert.equal(JSON.stringify(followUp).match(/live-transcript-must-appear-once/g)?.length, 1);
|
|
2415
|
+
assert.equal(result.usage.inputTokens, 30, 'warmup remains included in billable totals');
|
|
2416
|
+
assert.equal(result.usage.mainInputTokens, 20, 'main context snapshot remains separate');
|
|
2417
|
+
} finally {
|
|
2418
|
+
for (const [name, value] of Object.entries(savedEnv)) {
|
|
2419
|
+
if (value == null) delete process.env[name];
|
|
2420
|
+
else process.env[name] = value;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2263
2425
|
test('openai oauth ws delta: native tool_search output replays with canonical fields', () => {
|
|
2264
2426
|
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
2265
2427
|
try {
|
|
@@ -55,13 +55,26 @@ test('mature closed on-disk orphan absent from summary index gets tombstone-swep
|
|
|
55
55
|
};
|
|
56
56
|
writeFileSync(join(sessionsDir, `${orphan.id}.json`), JSON.stringify(orphan));
|
|
57
57
|
|
|
58
|
+
// (3) A shared-store tombstone whose id still has local in-flight work.
|
|
59
|
+
// It must be excluded before unlink, otherwise that work's late save
|
|
60
|
+
// could recreate an open session after the tombstone disappears.
|
|
61
|
+
const locallyLive = {
|
|
62
|
+
...orphan,
|
|
63
|
+
id: 'sess_shared_tombstone_locally_live',
|
|
64
|
+
};
|
|
65
|
+
writeFileSync(join(sessionsDir, `${locallyLive.id}.json`), JSON.stringify(locallyLive));
|
|
66
|
+
|
|
58
67
|
// Index contains ONLY the indexed session — the orphan is absent.
|
|
59
68
|
const rows = _writeSummaryIndex([_sessionSummary(indexed)]);
|
|
60
69
|
assert.equal(rows.length, 1, 'index seeded with exactly one row');
|
|
61
70
|
assert.ok(!rows.some((r) => r.id === orphan.id), 'orphan is absent from the summary index');
|
|
62
71
|
|
|
63
72
|
// Tombstone-only pass (no idle sweep) with a 1h maturity threshold.
|
|
64
|
-
const result = sweepStaleSessions({
|
|
73
|
+
const result = sweepStaleSessions({
|
|
74
|
+
sweepIdle: false,
|
|
75
|
+
tombstoneMaxAgeMs: ONE_HOUR,
|
|
76
|
+
isSessionLive: (id) => id === locallyLive.id,
|
|
77
|
+
});
|
|
65
78
|
|
|
66
79
|
assert.equal(result.tombstonesCleaned, 1, 'the orphan tombstone was reclaimed');
|
|
67
80
|
assert.ok(
|
|
@@ -76,6 +89,19 @@ test('mature closed on-disk orphan absent from summary index gets tombstone-swep
|
|
|
76
89
|
existsSync(join(sessionsDir, `${indexed.id}.json`)),
|
|
77
90
|
'fresh open indexed session was preserved',
|
|
78
91
|
);
|
|
92
|
+
assert.ok(
|
|
93
|
+
existsSync(join(sessionsDir, `${locallyLive.id}.json`)),
|
|
94
|
+
'locally live shared-store tombstone was excluded before unlink',
|
|
95
|
+
);
|
|
96
|
+
const afterLocalWork = sweepStaleSessions({
|
|
97
|
+
sweepIdle: false,
|
|
98
|
+
tombstoneMaxAgeMs: ONE_HOUR,
|
|
99
|
+
isSessionLive: () => false,
|
|
100
|
+
});
|
|
101
|
+
assert.ok(
|
|
102
|
+
afterLocalWork.tombstoneDetails.some((d) => d.id === locallyLive.id),
|
|
103
|
+
'protected tombstone is reclaimed once local work settles',
|
|
104
|
+
);
|
|
79
105
|
} finally {
|
|
80
106
|
try { rmSync(dataDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
81
107
|
delete process.env.MIXDOG_DATA_DIR;
|
package/src/lib/keychain-cjs.cjs
CHANGED
|
@@ -12,36 +12,46 @@ const KEYCHAIN_TIMEOUT_MS = Number(process.env.MIXDOG_KEYCHAIN_TIMEOUT_MS || 150
|
|
|
12
12
|
const POWERSHELL_TIMEOUT_MS = KEYCHAIN_TIMEOUT_MS;
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
|
-
// getSecret read cache —
|
|
15
|
+
// getSecret read cache — collapse repeated reads during a turn while bounding
|
|
16
|
+
// cross-process staleness. Misses are never cached, so a token saved by another
|
|
17
|
+
// process becomes visible immediately; hits expire so rotations are observed.
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
17
19
|
const KEYCHAIN_CACHE_TTL_MS = (() => {
|
|
18
|
-
const
|
|
19
|
-
return Number.isFinite(
|
|
20
|
+
const value = Number(process.env.MIXDOG_KEYCHAIN_CACHE_TTL_MS);
|
|
21
|
+
return Number.isFinite(value) && value >= 0 ? value : 30000;
|
|
20
22
|
})();
|
|
21
|
-
/** @type {Map<string, {value:
|
|
22
|
-
|
|
23
|
+
/** @type {Map<string, {value: string, expiresAt: number}>} */
|
|
24
|
+
const _secretCache = new Map();
|
|
23
25
|
|
|
24
26
|
function _cacheGet(account) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const entry = _secretCache.get(account);
|
|
27
|
+
const key = `${SERVICE}\0${account}`;
|
|
28
|
+
const entry = _secretCache.get(key);
|
|
28
29
|
if (!entry) return undefined;
|
|
29
|
-
if (Date.now()
|
|
30
|
-
_secretCache.delete(
|
|
30
|
+
if (entry.expiresAt <= Date.now()) {
|
|
31
|
+
_secretCache.delete(key);
|
|
31
32
|
return undefined;
|
|
32
33
|
}
|
|
33
|
-
return entry.value;
|
|
34
|
+
return entry.value;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function _cacheSet(account, value) {
|
|
37
|
-
if (KEYCHAIN_CACHE_TTL_MS
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (value == null || KEYCHAIN_CACHE_TTL_MS === 0) return;
|
|
39
|
+
_secretCache.set(`${SERVICE}\0${account}`, {
|
|
40
|
+
value,
|
|
41
|
+
expiresAt: Date.now() + KEYCHAIN_CACHE_TTL_MS,
|
|
42
|
+
});
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
function _cacheInvalidate(account) {
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
_secretCache.delete(`${SERVICE}\0${account}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function invalidateSecretCache(account) {
|
|
50
|
+
if (account == null) {
|
|
51
|
+
_secretCache.clear();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_cacheInvalidate(account);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
// CommonJS module: cannot import the ESM src/shared/wsl.mjs, so inline an
|
|
@@ -332,13 +342,16 @@ function hasSecret(account) {
|
|
|
332
342
|
}
|
|
333
343
|
|
|
334
344
|
function setSecret(account, value) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
try {
|
|
346
|
+
switch (platform()) {
|
|
347
|
+
case 'darwin': darwinSet(account, value); break;
|
|
348
|
+
case 'linux': linuxSet(account, value); break;
|
|
349
|
+
case 'win32': win32Set(account, value); break;
|
|
350
|
+
default: throw new Error(`[keychain] unsupported platform: ${process.platform}`);
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
_cacheInvalidate(account);
|
|
340
354
|
}
|
|
341
|
-
_cacheSet(account, value);
|
|
342
355
|
}
|
|
343
356
|
|
|
344
357
|
function deleteSecret(account) {
|
|
@@ -354,4 +367,4 @@ function deleteSecret(account) {
|
|
|
354
367
|
}
|
|
355
368
|
}
|
|
356
369
|
|
|
357
|
-
module.exports = { getSecret, setSecret, deleteSecret, hasSecret, SERVICE };
|
|
370
|
+
module.exports = { getSecret, setSecret, deleteSecret, hasSecret, invalidateSecretCache, SERVICE };
|