mixdog 0.9.147 → 0.9.149
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 +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -354,9 +354,40 @@ export class GeminiProvider {
|
|
|
354
354
|
_attachGeminiCacheState(opts, globalCache, currentIter);
|
|
355
355
|
return globalCache.cacheName;
|
|
356
356
|
}
|
|
357
|
+
// Wait on a shared create while still honouring THIS caller's abort:
|
|
358
|
+
// the create itself is process-global and must outlive any single
|
|
359
|
+
// session, so the caller only stops waiting (resolving null) instead of
|
|
360
|
+
// cancelling the work for everyone.
|
|
361
|
+
const awaitSharedCreate = (task) => {
|
|
362
|
+
const signal = opts.signal;
|
|
363
|
+
if (!(signal instanceof AbortSignal)) return task.then((v) => v, () => null);
|
|
364
|
+
if (signal.aborted) return Promise.resolve(null);
|
|
365
|
+
return new Promise((resolve) => {
|
|
366
|
+
const onAbort = () => resolve(null);
|
|
367
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
368
|
+
task.then(
|
|
369
|
+
(value) => { signal.removeEventListener('abort', onAbort); resolve(value); },
|
|
370
|
+
() => { signal.removeEventListener('abort', onAbort); resolve(null); },
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
// Strict singleflight: a WAITER never inherits the creation duty.
|
|
375
|
+
//
|
|
376
|
+
// The earlier bounded wait loop still herded. When the shared create
|
|
377
|
+
// settles, its own cleanup removes the slot BEFORE the waiters resume,
|
|
378
|
+
// so every waiter saw an empty slot; and a waiter that exhausted its
|
|
379
|
+
// rounds fell through and created even while another create was in
|
|
380
|
+
// flight — 8 waiters over repeated failures produced ~4 concurrent
|
|
381
|
+
// cachedContents POSTs for one prefix.
|
|
382
|
+
//
|
|
383
|
+
// The rule is now unconditional: if a create is in flight we wait for
|
|
384
|
+
// exactly that one, and if it does not yield a cache we proceed
|
|
385
|
+
// UNCACHED (the cache is an optimization, and the next turn re-creates).
|
|
386
|
+
// Only the caller that finds an EMPTY slot creates, and it publishes its
|
|
387
|
+
// task synchronously below, so at most one create per key can exist.
|
|
357
388
|
const inFlightCreate = geminiGlobalCacheCreates.get(globalCacheKey);
|
|
358
389
|
if (inFlightCreate) {
|
|
359
|
-
const created = await inFlightCreate
|
|
390
|
+
const created = await awaitSharedCreate(inFlightCreate);
|
|
360
391
|
if (created?.cacheName) {
|
|
361
392
|
try {
|
|
362
393
|
appendAgentTrace({
|
|
@@ -374,6 +405,14 @@ export class GeminiProvider {
|
|
|
374
405
|
_attachGeminiCacheState(opts, created, currentIter);
|
|
375
406
|
return created.cacheName;
|
|
376
407
|
}
|
|
408
|
+
// Failed or abandoned wait: another caller may still have published
|
|
409
|
+
// a usable cache for this exact prefix meanwhile.
|
|
410
|
+
const raced = _getGeminiGlobalCache(globalCacheKey, Date.now());
|
|
411
|
+
if (raced) {
|
|
412
|
+
_attachGeminiCacheState(opts, raced, currentIter);
|
|
413
|
+
return raced.cacheName;
|
|
414
|
+
}
|
|
415
|
+
return canAttachState ? state.cacheName : null;
|
|
377
416
|
}
|
|
378
417
|
const createTask = (async () => {
|
|
379
418
|
try {
|
|
@@ -398,12 +437,14 @@ export class GeminiProvider {
|
|
|
398
437
|
body.contents = cachePrefixContents;
|
|
399
438
|
}
|
|
400
439
|
const url = `https://generativelanguage.googleapis.com/v1beta/cachedContents?key=${encodeURIComponent(apiKey)}`;
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
440
|
+
// Deliberately NOT merged with opts.signal. This create is the
|
|
441
|
+
// process-global singleflight every concurrent session waits on, so
|
|
442
|
+
// the FIRST caller's abort (stall-watchdog / closeSession) must not
|
|
443
|
+
// cancel the cache every other session is waiting for — that is the
|
|
444
|
+
// herd trigger. The 20s ceiling still bounds the preflight request,
|
|
445
|
+
// and an aborting caller simply stops waiting (awaitSharedCreate).
|
|
405
446
|
const createTotal = createTimeoutSignal(
|
|
406
|
-
|
|
447
|
+
null,
|
|
407
448
|
PROVIDER_CACHE_CREATE_TOTAL_TIMEOUT_MS,
|
|
408
449
|
'Gemini cachedContents.create total',
|
|
409
450
|
);
|
|
@@ -520,18 +561,23 @@ export class GeminiProvider {
|
|
|
520
561
|
}
|
|
521
562
|
})();
|
|
522
563
|
geminiGlobalCacheCreates.set(globalCacheKey, createTask);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
_attachGeminiCacheState(opts, created, currentIter);
|
|
529
|
-
return created.cacheName;
|
|
530
|
-
} finally {
|
|
564
|
+
// Ownership is released by the TASK, never by the awaiting caller: a
|
|
565
|
+
// caller that walks away (abort) must not retract a still-running create
|
|
566
|
+
// from the map, or the next caller starts a duplicate POST for the same
|
|
567
|
+
// prefix — the herd this singleflight exists to prevent.
|
|
568
|
+
createTask.finally(() => {
|
|
531
569
|
if (geminiGlobalCacheCreates.get(globalCacheKey) === createTask) {
|
|
532
570
|
geminiGlobalCacheCreates.delete(globalCacheKey);
|
|
533
571
|
}
|
|
534
|
-
}
|
|
572
|
+
});
|
|
573
|
+
// Owner path: same abort semantics as the waiters — the shared create
|
|
574
|
+
// continues even if this caller stops waiting.
|
|
575
|
+
const created = await awaitSharedCreate(createTask);
|
|
576
|
+
// A failed refresh must not silently retain a cache that may have
|
|
577
|
+
// expired or been evicted server-side. The caller proceeds uncached.
|
|
578
|
+
if (!created?.cacheName) return null;
|
|
579
|
+
_attachGeminiCacheState(opts, created, currentIter);
|
|
580
|
+
return created.cacheName;
|
|
535
581
|
}
|
|
536
582
|
|
|
537
583
|
async send(messages, model, tools, sendOpts) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { providerNativeToolPrefixCount } from '../../../../../session-runtime/provider-request-tools.mjs';
|
|
2
2
|
import { isNativeServerToolBlockType } from './anthropic-native-blocks.mjs';
|
|
3
|
+
import { sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../../session/context-utils.mjs';
|
|
4
|
+
import { normalizeContentForAnthropic } from '../media-normalization.mjs';
|
|
5
|
+
import {
|
|
6
|
+
anthropicFallbackProviderMetadata,
|
|
7
|
+
parseAnthropicFallbackBlock,
|
|
8
|
+
} from '../anthropic-server-fallback.mjs';
|
|
3
9
|
|
|
4
10
|
export const ANTHROPIC_CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
|
|
5
11
|
export const ANTHROPIC_CACHE_TTL_VOLATILE = { type: 'ephemeral' };
|
|
@@ -47,6 +53,117 @@ export function resolveAnthropicCacheTtls(opts) {
|
|
|
47
53
|
return resolved;
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
// Message-tail cache budget, shared by both Anthropic providers.
|
|
57
|
+
//
|
|
58
|
+
// 4-BP layout: the tools breakpoint is dropped — the system BP covers the tools
|
|
59
|
+
// prefix via Anthropic's prompt-cache prefix semantics (order: tools → system →
|
|
60
|
+
// messages), which frees its slot for the messages tail. The system blocks hold
|
|
61
|
+
// BP1/BP2/BP3 (tier3), so every consumed slot is already counted there.
|
|
62
|
+
//
|
|
63
|
+
// Env override: ANTHROPIC_MSG_SLOTS=0 disables message caching entirely; any
|
|
64
|
+
// value >=1 first marks the previous user text turn so consecutive requests
|
|
65
|
+
// share a breakpoint, and a second free slot marks the tail for the newest
|
|
66
|
+
// delta. messageTtl === null (no slot, or ttls.messages disabled) turns the
|
|
67
|
+
// tail off.
|
|
68
|
+
export function resolveAnthropicMessageCacheSlots(systemBlocks, ttls) {
|
|
69
|
+
const usedSlots = systemBlocks.filter(b => b.cache_control).length;
|
|
70
|
+
const slotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
|
|
71
|
+
const defaultSlots = Math.max(0, 4 - usedSlots);
|
|
72
|
+
const messageSlots = ttls.messages
|
|
73
|
+
? (Number.isFinite(slotsCap) && slotsCap >= 0 ? Math.min(slotsCap, defaultSlots) : defaultSlots)
|
|
74
|
+
: 0;
|
|
75
|
+
// Key order matches the object both call sites built inline: messageTtl
|
|
76
|
+
// first, then messageSlots.
|
|
77
|
+
return { messageTtl: messageSlots > 0 ? ttls.messages : null, messageSlots };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Single lowering of orchestrator messages to the Anthropic wire shape. The
|
|
81
|
+
// API-key provider (anthropic.mjs, via anthropic-messages.mjs) and the OAuth
|
|
82
|
+
// provider (anthropic-oauth.mjs) both used to carry byte-identical copies;
|
|
83
|
+
// they now share this one.
|
|
84
|
+
//
|
|
85
|
+
// Marker-free lowering. cache_control is applied AFTER sanitization by
|
|
86
|
+
// applyAnthropicCacheMarkers() so that block drops/inserts/reorders performed
|
|
87
|
+
// by sanitizeAnthropicContentPairs cannot move or delete a marked block (the
|
|
88
|
+
// root cause of the sporadic COLD-turn cache miss: pre-sanitize markers landed
|
|
89
|
+
// on blocks the sanitizer then rewrote, so the provider-visible breakpoint
|
|
90
|
+
// diverged from the cached one).
|
|
91
|
+
export function toAnthropicMessages(messages) {
|
|
92
|
+
const result = [];
|
|
93
|
+
for (let idx = 0; idx < messages.length; idx++) {
|
|
94
|
+
const m = messages[idx];
|
|
95
|
+
if (m.role === 'system') continue;
|
|
96
|
+
if (m.role === 'assistant' && (m.toolCalls?.length || m.assistantBlocks?.length || m.thinkingBlocks?.length)) {
|
|
97
|
+
let content;
|
|
98
|
+
if (m.assistantBlocks?.length) {
|
|
99
|
+
content = m.assistantBlocks.slice();
|
|
100
|
+
} else {
|
|
101
|
+
content = [];
|
|
102
|
+
// Adaptive-thinking round-trip: prior-turn thinking blocks are
|
|
103
|
+
// REQUIRED back, unmodified (signature intact; empty thinking
|
|
104
|
+
// field allowed), and MUST precede tool_use blocks. Emit them
|
|
105
|
+
// first, verbatim as received from the SSE parser.
|
|
106
|
+
if (Array.isArray(m.thinkingBlocks)) {
|
|
107
|
+
for (const tb of m.thinkingBlocks) {
|
|
108
|
+
if (tb && typeof tb === 'object') content.push(tb);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (m.content) content.push({ type: 'text', text: m.content });
|
|
112
|
+
for (const tc of m.toolCalls || []) {
|
|
113
|
+
content.push({
|
|
114
|
+
type: 'tool_use',
|
|
115
|
+
id: tc.id,
|
|
116
|
+
name: tc.name,
|
|
117
|
+
input: tc.arguments,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result.push({ role: 'assistant', content });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (m.role === 'tool') {
|
|
125
|
+
const last = result[result.length - 1];
|
|
126
|
+
const native = m.nativeToolSearch;
|
|
127
|
+
const nativeProvider = String(native?.provider || '').toLowerCase();
|
|
128
|
+
const anthropicNative = new Set(['anthropic', 'anthropic-oauth']);
|
|
129
|
+
const references = (!nativeProvider || anthropicNative.has(nativeProvider))
|
|
130
|
+
&& Array.isArray(native?.toolReferences)
|
|
131
|
+
? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
132
|
+
: [];
|
|
133
|
+
const block = {
|
|
134
|
+
type: 'tool_result',
|
|
135
|
+
tool_use_id: m.toolCallId || '',
|
|
136
|
+
content: references.length
|
|
137
|
+
? references.map((tool_name) => ({ type: 'tool_reference', tool_name }))
|
|
138
|
+
: normalizeContentForAnthropic(m.content),
|
|
139
|
+
...((m.toolKind === 'error' || m.isError === true) ? { is_error: true } : {}),
|
|
140
|
+
};
|
|
141
|
+
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
142
|
+
last.content.push(block);
|
|
143
|
+
} else {
|
|
144
|
+
result.push({ role: 'user', content: [block] });
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// First-party client parity: fold a user text turn that directly follows a
|
|
149
|
+
// tool_result turn into that tool_result's content. A sibling text turn
|
|
150
|
+
// after tool_result renders as `</function_results>\n\nHuman:` on the wire
|
|
151
|
+
// and trains the model toward 3-token empty end_turn completions (empty
|
|
152
|
+
// end_turn livelock prevention; see foldUserTextIntoToolResultTail).
|
|
153
|
+
// EXCEPTION: steering-origin user messages (human/TUI interjections)
|
|
154
|
+
// keep their own user turn so provenance survives — folding them would
|
|
155
|
+
// disguise user input as tool output. Anthropic accepts a user text
|
|
156
|
+
// message after a tool_result message, so the turn stays request-valid.
|
|
157
|
+
const isSteering = m.role === 'user' && m.meta?.source === 'steering';
|
|
158
|
+
if (m.role === 'user' && !isSteering
|
|
159
|
+
&& foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
|
|
163
|
+
}
|
|
164
|
+
return sanitizeAnthropicContentPairs(result);
|
|
165
|
+
}
|
|
166
|
+
|
|
50
167
|
export function clampAnthropicThinkingBudget(value, maxTokens) {
|
|
51
168
|
const desired = Math.floor(Number(value));
|
|
52
169
|
const max = Math.floor(Number(maxTokens));
|
|
@@ -63,6 +180,7 @@ const REPLAYABLE_ASSISTANT_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking
|
|
|
63
180
|
|
|
64
181
|
export function normalizeAnthropicNonStreamingResponse(message, fallbackModel = '') {
|
|
65
182
|
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
183
|
+
const fallbackEvents = blocks.map(parseAnthropicFallbackBlock).filter(Boolean);
|
|
66
184
|
const text = blocks
|
|
67
185
|
.filter((block) => block?.type === 'text')
|
|
68
186
|
.map((block) => String(block.text || ''))
|
|
@@ -99,13 +217,14 @@ export function normalizeAnthropicNonStreamingResponse(message, fallbackModel =
|
|
|
99
217
|
const cacheWrite = Number(usage.cache_creation_input_tokens) || 0;
|
|
100
218
|
return {
|
|
101
219
|
content: text,
|
|
102
|
-
model: message?.model || fallbackModel,
|
|
220
|
+
model: fallbackEvents.at(-1)?.fallbackModel || message?.model || fallbackModel,
|
|
103
221
|
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
104
222
|
stopReason: message?.stop_reason || null,
|
|
105
223
|
hasThinkingContent: thinkingBlocks.length > 0,
|
|
106
224
|
contentBlockTypes: blocks.map((block) => block?.type).filter(Boolean),
|
|
107
225
|
thinkingBlocks: thinkingBlocks.length ? thinkingBlocks : undefined,
|
|
108
226
|
assistantBlocks: nativeAssistantBlocks.length ? nativeAssistantBlocks : undefined,
|
|
227
|
+
providerMetadata: anthropicFallbackProviderMetadata(fallbackEvents),
|
|
109
228
|
usage: {
|
|
110
229
|
inputTokens: input,
|
|
111
230
|
outputTokens: Number(usage.output_tokens) || 0,
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// Shared env-boolean parser: unset/empty keeps `fallback`, and only the
|
|
2
|
+
// documented off-values flip a default-on flag. Single implementation behind
|
|
3
|
+
// the historical `_envFlag` name used by the OpenAI/xAI transport gates.
|
|
4
|
+
export function envFlag(name, fallback = true) {
|
|
5
|
+
const raw = process.env[name];
|
|
6
|
+
if (raw == null || raw === '') return fallback;
|
|
7
|
+
return !['0', 'false', 'off', 'no'].includes(String(raw).toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export function envPositiveInt(name, fallback) {
|
|
2
11
|
const raw = process.env[name];
|
|
3
12
|
if (raw == null || raw === '') return fallback;
|
|
@@ -277,9 +277,6 @@ export function readStreamOutcome(...sources) {
|
|
|
277
277
|
return _finalize(signals || EMPTY_SIGNALS)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
/** Alias kept for callers that read a provider midState directly. */
|
|
281
|
-
export const streamOutcomeFromState = readStreamOutcome
|
|
282
|
-
|
|
283
280
|
/**
|
|
284
281
|
* Attach the canonical record to `target` (usually a thrown error) and mirror
|
|
285
282
|
* it onto the historical alias flags. Aliases are only ever SET, never
|
|
@@ -1,13 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
|
+
import { cursorTokenExpiry } from './cursor-auth.mjs';
|
|
4
6
|
|
|
5
7
|
const ANTHROPIC_DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Read one credential file with THREE distinguishable outcomes:
|
|
11
|
+
* { present: false } → the file is not there (logged out)
|
|
12
|
+
* { present: true, unreadable: true } → it exists but cannot be read/parsed
|
|
13
|
+
* right now (EACCES, lock contention,
|
|
14
|
+
* a torn write mid atomic-replace)
|
|
15
|
+
* { present: true, data } → parsed content
|
|
16
|
+
* Collapsing the middle case into "absent" is what let a momentary FS failure
|
|
17
|
+
* be mistaken for a deliberate logout by every caller downstream.
|
|
18
|
+
*/
|
|
19
|
+
function readCredentialFile(path) {
|
|
20
|
+
if (!path) return { present: false, data: null, unreadable: false };
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(path)) return { present: false, data: null, unreadable: false };
|
|
24
|
+
raw = readFileSync(path, 'utf-8');
|
|
25
|
+
} catch {
|
|
26
|
+
return { present: true, data: null, unreadable: true };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return { present: true, data: JSON.parse(raw), unreadable: false };
|
|
30
|
+
} catch {
|
|
31
|
+
return { present: true, data: null, unreadable: true };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve a probe to 'present' | 'absent' | 'unreadable'. `evaluate` sees only
|
|
36
|
+
// the documents that parsed; if none of them yields usable credentials while at
|
|
37
|
+
// least one candidate file was unreadable, the answer is "cannot tell", never
|
|
38
|
+
// "absent".
|
|
39
|
+
function resolveProbeState(paths, evaluate) {
|
|
40
|
+
let sawUnreadable = false;
|
|
41
|
+
const docs = [];
|
|
42
|
+
for (const path of paths) {
|
|
43
|
+
if (!path) continue;
|
|
44
|
+
const file = readCredentialFile(path);
|
|
45
|
+
if (file.unreadable) { sawUnreadable = true; continue; }
|
|
46
|
+
if (file.data) docs.push(file.data);
|
|
47
|
+
}
|
|
48
|
+
if (evaluate(docs) === true) return 'present';
|
|
49
|
+
return sawUnreadable ? 'unreadable' : 'absent';
|
|
11
50
|
}
|
|
12
51
|
|
|
13
52
|
function pushUnique(list, value) {
|
|
@@ -17,113 +56,177 @@ function pushUnique(list, value) {
|
|
|
17
56
|
|
|
18
57
|
// Short-TTL memo. Under simultaneous multi-agent launch, buildDefaultConfig
|
|
19
58
|
// and registry lazy-init call these probes in bursts; each probe does
|
|
20
|
-
// synchronous existsSync + readFileSync disk work. Cache the
|
|
59
|
+
// synchronous existsSync + readFileSync disk work. Cache the STATE per key
|
|
21
60
|
// for a brief window so one launch burst shares a single read instead of N.
|
|
22
61
|
// Kept short so the registry's "re-probe on each getProvider miss" self-heal
|
|
23
62
|
// (a credential file appearing/changing) is delayed by at most PROBE_TTL_MS.
|
|
24
63
|
const PROBE_TTL_MS = 3000;
|
|
25
64
|
const _probeCache = new Map();
|
|
26
|
-
|
|
65
|
+
|
|
66
|
+
// Cheap change-detector for the candidate credential files. statSync is orders
|
|
67
|
+
// of magnitude cheaper than the readFileSync + JSON.parse the memo exists to
|
|
68
|
+
// coalesce, so it runs on every call: the memo may only return a cached answer
|
|
69
|
+
// while the files it was computed from are byte-for-byte the same. Without it a
|
|
70
|
+
// logout stayed invisible for up to PROBE_TTL_MS — long enough for one
|
|
71
|
+
// immediate config reload to re-observe 'present' and keep a revoked provider
|
|
72
|
+
// registered. Removal, appearance and rewrite are all observed immediately.
|
|
73
|
+
//
|
|
74
|
+
// mtimeMs+size alone missed a same-length rewrite that preserved (or restored)
|
|
75
|
+
// the timestamp — exactly the shape of a token refresh written by another
|
|
76
|
+
// process, or of an atomic replace. ctimeMs is bumped by the write itself and
|
|
77
|
+
// cannot be back-dated through utimes, and ino changes on rename-replace, so
|
|
78
|
+
// both join the signature; neither costs an extra syscall.
|
|
79
|
+
function credentialPathsSignature(paths, extra = '') {
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (const path of paths) {
|
|
82
|
+
if (!path) continue;
|
|
83
|
+
try {
|
|
84
|
+
const st = statSync(path);
|
|
85
|
+
parts.push(`${path}:${st.mtimeMs}:${st.ctimeMs}:${st.size}:${st.ino}`);
|
|
86
|
+
} catch {
|
|
87
|
+
// Missing (the logout case) or unstattable — both distinct from a
|
|
88
|
+
// readable file, and both must invalidate a cached answer.
|
|
89
|
+
parts.push(`${path}:-`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (extra) parts.push(extra);
|
|
93
|
+
return parts.join('|');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function memoProbe(key, paths, compute, extraSignature = '') {
|
|
97
|
+
const signature = credentialPathsSignature(paths, extraSignature);
|
|
27
98
|
const hit = _probeCache.get(key);
|
|
28
99
|
const now = Date.now();
|
|
29
|
-
if (hit && now - hit.ts < PROBE_TTL_MS) return hit.value;
|
|
100
|
+
if (hit && hit.signature === signature && now - hit.ts < PROBE_TTL_MS) return hit.value;
|
|
30
101
|
const value = compute();
|
|
31
|
-
_probeCache.set(key, { ts: now, value });
|
|
102
|
+
_probeCache.set(key, { ts: now, signature, value });
|
|
32
103
|
return value;
|
|
33
104
|
}
|
|
34
105
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
106
|
+
function anthropicOAuthState() {
|
|
107
|
+
const paths = [];
|
|
108
|
+
pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
109
|
+
pushUnique(paths, ANTHROPIC_DEFAULT_CREDENTIALS_PATH);
|
|
110
|
+
return memoProbe('anthropic-oauth', paths, () => {
|
|
111
|
+
return resolveProbeState(paths, (docs) => {
|
|
112
|
+
const candidates = [];
|
|
113
|
+
for (const raw of docs) {
|
|
43
114
|
const oauth = raw?.claudeAiOauth;
|
|
44
115
|
if (oauth?.accessToken) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
candidates.push({
|
|
117
|
+
accessToken: oauth.accessToken,
|
|
118
|
+
refreshToken: oauth.refreshToken || null,
|
|
119
|
+
expiresAt: Number(oauth.expiresAt ?? oauth.expires_at) || 0,
|
|
120
|
+
scopes: Array.isArray(oauth.scopes) ? oauth.scopes : [],
|
|
121
|
+
});
|
|
51
122
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
123
|
+
}
|
|
124
|
+
if (!candidates.length) return false;
|
|
125
|
+
candidates.sort((a, b) => (Number(b.expiresAt) || 0) - (Number(a.expiresAt) || 0));
|
|
126
|
+
const chosen = candidates[0];
|
|
127
|
+
const hasInferenceScope = Array.isArray(chosen.scopes) && chosen.scopes.includes('user:inference');
|
|
128
|
+
const expiresAt = Number(chosen.expiresAt) || 0;
|
|
129
|
+
return !!(chosen.accessToken && hasInferenceScope
|
|
130
|
+
&& (expiresAt === 0 || expiresAt > Date.now() || chosen.refreshToken));
|
|
131
|
+
});
|
|
60
132
|
});
|
|
61
133
|
}
|
|
62
134
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
});
|
|
135
|
+
function openAIOAuthState() {
|
|
136
|
+
const paths = [
|
|
137
|
+
process.env.OPENAI_OAUTH_CREDENTIALS_PATH,
|
|
138
|
+
join(resolvePluginData(), 'openai-oauth.json'),
|
|
139
|
+
];
|
|
140
|
+
return memoProbe('openai-oauth', paths, () => resolveProbeState(
|
|
141
|
+
paths,
|
|
142
|
+
(docs) => docs.some((raw) => !!(raw?.access_token && raw?.refresh_token)),
|
|
143
|
+
));
|
|
75
144
|
}
|
|
76
145
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
});
|
|
146
|
+
function grokOAuthState() {
|
|
147
|
+
const paths = [
|
|
148
|
+
process.env.GROK_OAUTH_CREDENTIALS_PATH,
|
|
149
|
+
join(resolvePluginData(), 'grok-oauth.json'),
|
|
150
|
+
];
|
|
151
|
+
return memoProbe('grok-oauth', paths, () => resolveProbeState(
|
|
152
|
+
paths,
|
|
153
|
+
(docs) => docs.some((own) => !!(own?.access_token && own?.refresh_token)),
|
|
154
|
+
));
|
|
88
155
|
}
|
|
89
156
|
|
|
90
157
|
// Antigravity needs a resolved Cloud project alongside the tokens: without it
|
|
91
158
|
// every request 400s, so a half-finished login must not enable the provider.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
159
|
+
function antigravityOAuthState() {
|
|
160
|
+
const paths = [
|
|
161
|
+
process.env.ANTIGRAVITY_OAUTH_CREDENTIALS_PATH,
|
|
162
|
+
join(resolvePluginData(), 'antigravity-oauth.json'),
|
|
163
|
+
];
|
|
164
|
+
return memoProbe('antigravity-oauth', paths, () => resolveProbeState(
|
|
165
|
+
paths,
|
|
166
|
+
(docs) => docs.some((own) => !!(own?.access_token && own?.refresh_token && own?.project_id)),
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Content fingerprint of CURSOR_ACCESS_TOKEN (never the token itself).
|
|
171
|
+
// '-' means unset, which must stay distinguishable from any hash so that
|
|
172
|
+
// deleting the variable invalidates the memo immediately.
|
|
173
|
+
function cursorEnvTokenFingerprint() {
|
|
174
|
+
const token = process.env.CURSOR_ACCESS_TOKEN;
|
|
175
|
+
if (!token) return '-';
|
|
176
|
+
return createHash('sha256').update(String(token)).digest('base64url').slice(0, 22);
|
|
103
177
|
}
|
|
104
178
|
|
|
105
|
-
|
|
106
|
-
|
|
179
|
+
function cursorOAuthState() {
|
|
180
|
+
const paths = [
|
|
181
|
+
process.env.CURSOR_OAUTH_CREDENTIALS_PATH,
|
|
182
|
+
join(resolvePluginData(), 'cursor-oauth.json'),
|
|
183
|
+
];
|
|
184
|
+
// The env token is the whole answer on this branch and has no file metadata
|
|
185
|
+
// behind it, so its VALUE is what the memo signature has to track: a length
|
|
186
|
+
// (or a bare present/absent flag) let a swap to an equal-length expired token
|
|
187
|
+
// keep serving the stale 'present' for up to PROBE_TTL_MS. Hashed rather than
|
|
188
|
+
// embedded so the memo never holds a second copy of the raw credential.
|
|
189
|
+
const envSignature = `env:${cursorEnvTokenFingerprint()}`;
|
|
190
|
+
return memoProbe('cursor-oauth', paths, () => {
|
|
191
|
+
// An env-provided token needs no file read, so it is never "unreadable".
|
|
192
|
+
// Expiry is read by the one shared JWT reader (cursor-auth.cursorTokenExpiry),
|
|
193
|
+
// which answers 0 for a non-JWT or unparseable token — exactly the "assume
|
|
194
|
+
// usable" outcome this branch already took for those shapes.
|
|
107
195
|
if (process.env.CURSOR_ACCESS_TOKEN) {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
112
|
-
const expiresAt = Number(payload?.exp) > 0 ? Number(payload.exp) * 1000 : 0;
|
|
113
|
-
return expiresAt === 0 || expiresAt > Date.now();
|
|
114
|
-
} catch {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
196
|
+
const expiresAt = cursorTokenExpiry(process.env.CURSOR_ACCESS_TOKEN);
|
|
197
|
+
return (expiresAt === 0 || expiresAt > Date.now()) ? 'present' : 'absent';
|
|
117
198
|
}
|
|
118
|
-
|
|
119
|
-
process.env.CURSOR_OAUTH_CREDENTIALS_PATH,
|
|
120
|
-
join(resolvePluginData(), 'cursor-oauth.json'),
|
|
121
|
-
].filter(Boolean);
|
|
122
|
-
return paths.some((path) => {
|
|
123
|
-
const own = readJsonIfExists(path);
|
|
199
|
+
return resolveProbeState(paths, (docs) => docs.some((own) => {
|
|
124
200
|
if (!own?.access_token) return false;
|
|
125
201
|
const expiresAt = Number(own.expires_at) || 0;
|
|
126
202
|
return expiresAt === 0 || expiresAt > Date.now() || Boolean(own.refresh_token);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
203
|
+
}));
|
|
204
|
+
}, envSignature);
|
|
129
205
|
}
|
|
206
|
+
|
|
207
|
+
const OAUTH_PROBE_STATES = new Map([
|
|
208
|
+
['anthropic-oauth', anthropicOAuthState],
|
|
209
|
+
['openai-oauth', openAIOAuthState],
|
|
210
|
+
['grok-oauth', grokOAuthState],
|
|
211
|
+
['cursor-oauth', cursorOAuthState],
|
|
212
|
+
['antigravity-oauth', antigravityOAuthState],
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Tri-state credential probe: 'present' | 'absent' | 'unreadable'.
|
|
217
|
+
*
|
|
218
|
+
* Callers that must tell a deliberate logout ('absent') from a momentary FS
|
|
219
|
+
* failure ('unreadable') use this; `has*OAuthCredentials()` remains the plain
|
|
220
|
+
* "can we use it right now" boolean and is exactly `state === 'present'`.
|
|
221
|
+
*/
|
|
222
|
+
export function oauthCredentialProbeState(name) {
|
|
223
|
+
const probe = OAUTH_PROBE_STATES.get(String(name || ''));
|
|
224
|
+
if (!probe) return 'absent';
|
|
225
|
+
try { return probe(); } catch { return 'unreadable'; }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function hasAnthropicOAuthCredentials() { return anthropicOAuthState() === 'present'; }
|
|
229
|
+
export function hasOpenAIOAuthCredentials() { return openAIOAuthState() === 'present'; }
|
|
230
|
+
export function hasGrokOAuthCredentials() { return grokOAuthState() === 'present'; }
|
|
231
|
+
export function hasAntigravityOAuthCredentials() { return antigravityOAuthState() === 'present'; }
|
|
232
|
+
export function hasCursorOAuthCredentials() { return cursorOAuthState() === 'present'; }
|