neoagent 3.2.1-beta.0 → 3.2.1-beta.10

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.
Files changed (175) hide show
  1. package/docs/agent-run-lifecycle.md +10 -0
  2. package/extensions/chrome-browser/background.mjs +318 -88
  3. package/extensions/chrome-browser/http.mjs +136 -0
  4. package/extensions/chrome-browser/protocol.mjs +654 -90
  5. package/flutter_app/lib/main_chat.dart +118 -739
  6. package/flutter_app/lib/main_controller.dart +111 -20
  7. package/flutter_app/lib/main_integrations.dart +607 -8
  8. package/flutter_app/lib/main_models.dart +3 -0
  9. package/flutter_app/lib/main_operations.dart +334 -321
  10. package/flutter_app/lib/main_security.dart +266 -112
  11. package/flutter_app/lib/main_settings.dart +4 -3
  12. package/flutter_app/lib/main_shared.dart +14 -11
  13. package/flutter_app/lib/src/backend_client.dart +78 -0
  14. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  15. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  16. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  17. package/landing/index.html +3 -1
  18. package/lib/manager.js +106 -89
  19. package/lib/schema_migrations.js +145 -13
  20. package/package.json +30 -15
  21. package/runtime/paths.js +49 -5
  22. package/server/db/database.js +4 -4
  23. package/server/guest-agent.cli.package.json +13 -0
  24. package/server/guest_agent.js +85 -40
  25. package/server/http/middleware.js +24 -0
  26. package/server/http/routes.js +11 -6
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  29. package/server/public/flutter_bootstrap.js +2 -2
  30. package/server/public/main.dart.js +73083 -72209
  31. package/server/routes/admin.js +1 -1
  32. package/server/routes/agents.js +35 -2
  33. package/server/routes/android.js +30 -34
  34. package/server/routes/browser.js +23 -15
  35. package/server/routes/desktop.js +18 -1
  36. package/server/routes/integrations.js +107 -1
  37. package/server/routes/memory.js +1 -0
  38. package/server/routes/settings.js +16 -5
  39. package/server/routes/social_reach.js +12 -3
  40. package/server/routes/social_video.js +4 -0
  41. package/server/services/agents/manager.js +1 -1
  42. package/server/services/ai/capabilityHealth.js +62 -96
  43. package/server/services/ai/compaction.js +7 -2
  44. package/server/services/ai/history.js +45 -6
  45. package/server/services/ai/integrated_tools/http_request.js +8 -0
  46. package/server/services/ai/loop/agent_engine_core.js +496 -166
  47. package/server/services/ai/loop/blank_recovery.js +5 -4
  48. package/server/services/ai/loop/callbacks.js +1 -0
  49. package/server/services/ai/loop/completion_judge.js +121 -5
  50. package/server/services/ai/loop/conversation_loop.js +620 -340
  51. package/server/services/ai/loop/lifecycle.js +108 -0
  52. package/server/services/ai/loop/messaging_delivery.js +129 -57
  53. package/server/services/ai/loop/model_call_guard.js +91 -0
  54. package/server/services/ai/loop/model_io.js +48 -56
  55. package/server/services/ai/loop/progress_classification.js +2 -0
  56. package/server/services/ai/loop/tool_dispatch.js +28 -8
  57. package/server/services/ai/loopPolicy.js +48 -21
  58. package/server/services/ai/messagingFallback.js +17 -17
  59. package/server/services/ai/model_discovery.js +227 -0
  60. package/server/services/ai/model_failure_cache.js +108 -0
  61. package/server/services/ai/model_identity.js +71 -0
  62. package/server/services/ai/models.js +68 -163
  63. package/server/services/ai/providerRetry.js +17 -59
  64. package/server/services/ai/provider_selector.js +166 -0
  65. package/server/services/ai/providers/anthropic.js +4 -4
  66. package/server/services/ai/providers/claudeCode.js +21 -33
  67. package/server/services/ai/providers/githubCopilot.js +41 -20
  68. package/server/services/ai/providers/google.js +135 -97
  69. package/server/services/ai/providers/grok.js +6 -5
  70. package/server/services/ai/providers/grokOauth.js +19 -27
  71. package/server/services/ai/providers/nvidia.js +12 -7
  72. package/server/services/ai/providers/ollama.js +114 -86
  73. package/server/services/ai/providers/ollama_stream.js +142 -0
  74. package/server/services/ai/providers/openai.js +41 -7
  75. package/server/services/ai/providers/openaiCodex.js +13 -4
  76. package/server/services/ai/providers/openrouter.js +31 -9
  77. package/server/services/ai/providers/provider_error.js +36 -0
  78. package/server/services/ai/settings.js +26 -2
  79. package/server/services/ai/systemPrompt.js +19 -12
  80. package/server/services/ai/taskAnalysis.js +58 -10
  81. package/server/services/ai/terminal_reply.js +18 -0
  82. package/server/services/ai/toolEvidence.js +350 -29
  83. package/server/services/ai/tools.js +190 -111
  84. package/server/services/android/controller.js +770 -237
  85. package/server/services/android/process.js +140 -0
  86. package/server/services/android/sdk_download.js +143 -0
  87. package/server/services/android/uia.js +6 -5
  88. package/server/services/artifacts/store.js +24 -0
  89. package/server/services/browser/controller.js +843 -385
  90. package/server/services/browser/extension/gateway.js +40 -16
  91. package/server/services/browser/extension/protocol.js +15 -1
  92. package/server/services/browser/extension/provider.js +71 -47
  93. package/server/services/browser/extension/registry.js +155 -34
  94. package/server/services/cli/executor.js +62 -9
  95. package/server/services/credentials/bitwarden_cli.js +322 -0
  96. package/server/services/credentials/broker.js +594 -0
  97. package/server/services/desktop/gateway.js +41 -4
  98. package/server/services/desktop/protocol.js +3 -0
  99. package/server/services/desktop/provider.js +39 -42
  100. package/server/services/desktop/registry.js +137 -52
  101. package/server/services/integrations/bitwarden/constants.js +14 -0
  102. package/server/services/integrations/bitwarden/provider.js +197 -0
  103. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  104. package/server/services/integrations/figma/provider.js +78 -12
  105. package/server/services/integrations/github/common.js +11 -6
  106. package/server/services/integrations/github/provider.js +52 -53
  107. package/server/services/integrations/google/provider.js +55 -19
  108. package/server/services/integrations/home_assistant/network.js +17 -20
  109. package/server/services/integrations/home_assistant/provider.js +7 -5
  110. package/server/services/integrations/home_assistant/tools.js +17 -5
  111. package/server/services/integrations/http.js +51 -0
  112. package/server/services/integrations/manager.js +159 -53
  113. package/server/services/integrations/microsoft/provider.js +80 -13
  114. package/server/services/integrations/neoarchive/provider.js +55 -29
  115. package/server/services/integrations/neorecall/client.js +17 -10
  116. package/server/services/integrations/neorecall/provider.js +20 -11
  117. package/server/services/integrations/notion/provider.js +16 -13
  118. package/server/services/integrations/oauth_provider.js +115 -51
  119. package/server/services/integrations/registry.js +2 -0
  120. package/server/services/integrations/slack/provider.js +98 -9
  121. package/server/services/integrations/spotify/provider.js +67 -71
  122. package/server/services/integrations/trello/provider.js +21 -7
  123. package/server/services/integrations/weather/provider.js +18 -12
  124. package/server/services/integrations/whatsapp/provider.js +76 -16
  125. package/server/services/manager.js +110 -1
  126. package/server/services/memory/embedding_index.js +20 -8
  127. package/server/services/memory/embeddings.js +151 -90
  128. package/server/services/memory/ingestion.js +50 -9
  129. package/server/services/memory/ingestion_documents.js +13 -3
  130. package/server/services/memory/manager.js +52 -19
  131. package/server/services/messaging/automation.js +85 -10
  132. package/server/services/messaging/formatting_guides.js +7 -4
  133. package/server/services/messaging/http_platforms.js +33 -13
  134. package/server/services/messaging/inbound_queue.js +78 -24
  135. package/server/services/messaging/inbound_store.js +224 -0
  136. package/server/services/messaging/manager.js +326 -51
  137. package/server/services/messaging/typing_keepalive.js +5 -2
  138. package/server/services/messaging/whatsapp.js +22 -14
  139. package/server/services/network/http.js +210 -0
  140. package/server/services/network/safe_request.js +307 -0
  141. package/server/services/runtime/backends/local-vm.js +227 -67
  142. package/server/services/runtime/docker-vm-manager.js +9 -0
  143. package/server/services/runtime/guest_bootstrap.js +30 -4
  144. package/server/services/runtime/guest_image.js +43 -12
  145. package/server/services/runtime/manager.js +77 -23
  146. package/server/services/runtime/validation.js +7 -6
  147. package/server/services/security/tool_categories.js +6 -0
  148. package/server/services/social_reach/channels/github.js +10 -4
  149. package/server/services/social_reach/channels/reddit.js +4 -4
  150. package/server/services/social_reach/channels/rss.js +2 -2
  151. package/server/services/social_reach/channels/social_video.js +12 -7
  152. package/server/services/social_reach/channels/v2ex.js +21 -8
  153. package/server/services/social_reach/channels/x.js +2 -2
  154. package/server/services/social_reach/channels/xueqiu.js +5 -5
  155. package/server/services/social_reach/service.js +9 -6
  156. package/server/services/social_reach/utils.js +65 -14
  157. package/server/services/social_video/service.js +160 -50
  158. package/server/services/tasks/integration_runtime.js +18 -8
  159. package/server/services/tasks/runtime.js +39 -4
  160. package/server/services/voice/agentBridge.js +17 -4
  161. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  162. package/server/services/voice/liveSession.js +31 -0
  163. package/server/services/voice/message.js +1 -1
  164. package/server/services/voice/openaiSpeech.js +33 -8
  165. package/server/services/voice/providers.js +233 -151
  166. package/server/services/voice/runtime.js +2 -2
  167. package/server/services/voice/runtimeManager.js +118 -20
  168. package/server/services/voice/turnRunner.js +6 -0
  169. package/server/services/wearable/firmware_manifest.js +51 -13
  170. package/server/services/wearable/service.js +1 -0
  171. package/server/utils/abort.js +96 -0
  172. package/server/utils/cloud-security.js +110 -3
  173. package/server/utils/files.js +31 -0
  174. package/server/utils/image_payload.js +95 -0
  175. package/server/utils/retry.js +107 -0
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const { getAiSettings } = require('./settings');
4
+ const { createProviderInstance, getSupportedModels } = require('./models');
5
+ const {
6
+ getRawModelId,
7
+ normalizeModelSelections,
8
+ resolveModelSelection,
9
+ } = require('./model_identity');
10
+ const { isModelCoolingDown } = require('./model_failure_cache');
11
+
12
+ function buildSelection(model, userId, providerConfig) {
13
+ return {
14
+ provider: createProviderInstance(model.provider, userId, providerConfig),
15
+ model: getRawModelId(model),
16
+ modelSelectionId: model.id,
17
+ providerName: model.provider,
18
+ };
19
+ }
20
+
21
+ async function getProviderForUser(
22
+ userId,
23
+ _task = '',
24
+ isSubagent = false,
25
+ modelOverride = null,
26
+ providerConfig = {},
27
+ ) {
28
+ const agentId = providerConfig.agentId || null;
29
+ const aiSettings = getAiSettings(userId, agentId);
30
+ const models = await getSupportedModels(userId, agentId, {
31
+ signal: providerConfig.signal,
32
+ });
33
+ const selectableModels = models.filter((model) => model.available !== false);
34
+ const readyModels = selectableModels.filter(
35
+ (model) => !isModelCoolingDown(userId, agentId, model.id),
36
+ );
37
+
38
+ if (modelOverride && typeof modelOverride === 'string') {
39
+ const requested = resolveModelSelection(selectableModels, modelOverride);
40
+ if (!requested) {
41
+ throw new Error(`Requested model '${modelOverride.trim()}' is not available.`);
42
+ }
43
+ if (isModelCoolingDown(userId, agentId, requested.id) && readyModels.length > 0) {
44
+ const configuredFallback = resolveModelSelection(
45
+ readyModels,
46
+ aiSettings.fallback_model_id,
47
+ );
48
+ const fallback = configuredFallback
49
+ || readyModels.find((model) => model.provider !== requested.provider)
50
+ || readyModels[0];
51
+ providerConfig.onStatus?.({
52
+ phase: 'model_fallback',
53
+ message: `Skipping recently unavailable model ${requested.id}; using ${fallback.id}.`,
54
+ });
55
+ return buildSelection(fallback, userId, providerConfig);
56
+ }
57
+ return buildSelection(requested, userId, providerConfig);
58
+ }
59
+
60
+ const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
61
+ ? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
62
+ : [];
63
+ const enabledIds = normalizeModelSelections(selectableModels, configuredEnabledIds);
64
+ let availableModels = configuredEnabledIds.length > 0
65
+ ? readyModels.filter((model) => enabledIds.includes(model.id))
66
+ : readyModels;
67
+
68
+ if (
69
+ availableModels.length === 0
70
+ && configuredEnabledIds.length > 0
71
+ && enabledIds.length > 0
72
+ ) {
73
+ const configuredFallback = resolveModelSelection(
74
+ readyModels,
75
+ aiSettings.fallback_model_id,
76
+ );
77
+ if (configuredFallback) {
78
+ providerConfig.onStatus?.({
79
+ phase: 'model_fallback',
80
+ message: `Enabled models are temporarily unavailable; using ${configuredFallback.id}.`,
81
+ });
82
+ availableModels = [configuredFallback];
83
+ }
84
+ }
85
+
86
+ // If every configured model is cooling down and there is no alternative,
87
+ // allow one retry instead of making the installation permanently unavailable.
88
+ if (availableModels.length === 0 && readyModels.length === 0) {
89
+ availableModels = configuredEnabledIds.length > 0
90
+ ? selectableModels.filter((model) => enabledIds.includes(model.id))
91
+ : selectableModels;
92
+ }
93
+
94
+ if (availableModels.length === 0) {
95
+ const message = configuredEnabledIds.length > 0
96
+ ? 'None of the enabled AI models are currently available. Check model and provider settings.'
97
+ : 'No AI providers are currently available. Open Settings and configure at least one provider.';
98
+ throw new Error(message);
99
+ }
100
+
101
+ const fallbackModel = availableModels[0];
102
+ const userSelectedDefault = isSubagent
103
+ ? (aiSettings.default_subagent_model || 'auto')
104
+ : (aiSettings.default_chat_model || 'auto');
105
+ const smarterSelection = aiSettings.smarter_model_selector !== false
106
+ && aiSettings.smarter_model_selector !== 'false';
107
+
108
+ let selectedModel = fallbackModel;
109
+ if (userSelectedDefault !== 'auto') {
110
+ selectedModel = resolveModelSelection(availableModels, userSelectedDefault) || fallbackModel;
111
+ const configuredDefault = resolveModelSelection(selectableModels, userSelectedDefault);
112
+ if (
113
+ configuredDefault
114
+ && configuredDefault.id !== selectedModel.id
115
+ && isModelCoolingDown(userId, agentId, configuredDefault.id)
116
+ ) {
117
+ providerConfig.onStatus?.({
118
+ phase: 'model_fallback',
119
+ message: `Skipping recently unavailable model ${configuredDefault.id}; using ${selectedModel.id}.`,
120
+ });
121
+ }
122
+ } else {
123
+ const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
124
+ ? providerConfig.selectionHint
125
+ : {};
126
+ const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
127
+ const highAutonomy = selectionHint.autonomyLevel === 'high'
128
+ || selectionHint.complexity === 'complex';
129
+ const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
130
+ const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto')
131
+ .trim()
132
+ .toLowerCase();
133
+ const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
134
+ ? preferredPurpose
135
+ : '';
136
+ const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
137
+ const chooseForPurpose = (purpose) => {
138
+ const candidates = availableModels.filter((model) => model.purpose === purpose);
139
+ if (candidates.length === 0) return null;
140
+ if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
141
+ return [...candidates].sort((left, right) => (
142
+ (priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
143
+ ))[0];
144
+ }
145
+ if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
146
+ return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap')
147
+ || candidates[0];
148
+ }
149
+ return candidates[0];
150
+ };
151
+
152
+ if (smarterSelection && requestedPurpose) {
153
+ selectedModel = chooseForPurpose(requestedPurpose) || fallbackModel;
154
+ } else if (smarterSelection && highAutonomy) {
155
+ selectedModel = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
156
+ } else if (isSubagent) {
157
+ selectedModel = chooseForPurpose('fast') || fallbackModel;
158
+ } else {
159
+ selectedModel = chooseForPurpose('general') || fallbackModel;
160
+ }
161
+ }
162
+
163
+ return buildSelection(selectedModel, userId, providerConfig);
164
+ }
165
+
166
+ module.exports = { getProviderForUser };
@@ -31,8 +31,8 @@ class AnthropicProvider extends BaseProvider {
31
31
  });
32
32
  }
33
33
 
34
- async listModels() {
35
- const res = await this.client.models.list({ limit: 100 });
34
+ async listModels(signal = null) {
35
+ const res = await this.client.models.list({ limit: 100 }, { signal });
36
36
  return (res.data || []).map((m) => ({ id: m.id, name: m.display_name || m.id }));
37
37
  }
38
38
 
@@ -169,7 +169,7 @@ class AnthropicProvider extends BaseProvider {
169
169
 
170
170
  if (system.length > 0) params.system = system;
171
171
  if (tools.length > 0) params.tools = this.formatTools(tools);
172
- const response = await this.client.messages.create(params);
172
+ const response = await this.client.messages.create(params, { signal: options.signal });
173
173
  const responseBlocks = Array.isArray(response?.content)
174
174
  ? response.content
175
175
  : (response?.content && typeof response.content === 'object' ? [response.content] : []);
@@ -225,7 +225,7 @@ class AnthropicProvider extends BaseProvider {
225
225
 
226
226
  if (system.length > 0) params.system = system;
227
227
  if (tools.length > 0) params.tools = this.formatTools(tools);
228
- const stream = await this.client.messages.stream(params);
228
+ const stream = await this.client.messages.stream(params, { signal: options.signal });
229
229
 
230
230
  let content = '';
231
231
  let currentToolCalls = [];
@@ -2,6 +2,8 @@ const os = require('os');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const Anthropic = require('@anthropic-ai/sdk');
5
+ const { ENV_FILE, upsertEnvValue } = require('../../../../runtime/paths');
6
+ const { fetchResponseText } = require('../../network/http');
5
7
  const { AnthropicProvider } = require('./anthropic');
6
8
 
7
9
  const CLAUDE_CLI_CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
@@ -12,6 +14,8 @@ const CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI
12
14
  const CLAUDE_CODE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
13
15
  const CLAUDE_CODE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
14
16
  const CLAUDE_CODE_SCOPES = 'user:inference user:profile org:create_api_key user:sessions:claude_code user:mcp_servers';
17
+ const OAUTH_REFRESH_TIMEOUT_MS = 30000;
18
+ const OAUTH_MAX_RESPONSE_BYTES = 256 * 1024;
15
19
 
16
20
  function readTokenRecord(data) {
17
21
  const tokens = data?.claudeAiOauthTokens || data?.claudeAiOauth || {};
@@ -71,40 +75,17 @@ function normalizeExpiresAt(data) {
71
75
  return null;
72
76
  }
73
77
 
74
- function sanitizeEnvKey(key) {
75
- return String(key).replace(/[\r\n]/g, '');
76
- }
77
-
78
- function sanitizeEnvValue(value) {
79
- return String(value).replace(/[\r\n]/g, '');
80
- }
81
-
82
78
  function persistEnvValue(key, value) {
83
79
  if (!value) return;
84
80
  try {
85
- const { ENV_FILE } = require('../../../../runtime/paths');
86
- const safeKey = sanitizeEnvKey(key);
87
- const safeValue = sanitizeEnvValue(value);
88
- const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
89
- const lines = raw ? raw.split('\n') : [];
90
- let replaced = false;
91
- for (let i = 0; i < lines.length; i++) {
92
- if (lines[i].startsWith(`${safeKey}=`)) {
93
- lines[i] = `${safeKey}=${safeValue}`;
94
- replaced = true;
95
- break;
96
- }
97
- }
98
- if (!replaced) lines.push(`${safeKey}=${safeValue}`);
99
- const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
100
- fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
101
- fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
81
+ upsertEnvValue(ENV_FILE, key, value);
102
82
  } catch { }
103
83
  }
104
84
 
105
- async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
85
+ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, signal = null) {
106
86
  if (!refreshToken) return null;
107
- const response = await fetchImpl(CLAUDE_CODE_TOKEN_URL, {
87
+ const { response, text } = await fetchResponseText(CLAUDE_CODE_TOKEN_URL, {
88
+ fetchImpl,
108
89
  method: 'POST',
109
90
  headers: {
110
91
  'Content-Type': 'application/json',
@@ -116,9 +97,14 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
116
97
  refresh_token: refreshToken,
117
98
  client_id: CLAUDE_CODE_CLIENT_ID,
118
99
  }),
100
+ signal,
101
+ timeoutMs: OAUTH_REFRESH_TIMEOUT_MS,
102
+ maxResponseBytes: OAUTH_MAX_RESPONSE_BYTES,
103
+ serviceName: 'Claude Code OAuth refresh',
104
+ timeoutCode: 'PROVIDER_OAUTH_TIMEOUT',
105
+ tooLargeCode: 'PROVIDER_OAUTH_RESPONSE_TOO_LARGE',
119
106
  });
120
107
 
121
- const text = await response.text();
122
108
  let data = {};
123
109
  try {
124
110
  data = text ? JSON.parse(text) : {};
@@ -127,7 +113,9 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
127
113
  }
128
114
 
129
115
  if (!response.ok) {
130
- const detail = data?.error?.message || data?.error_description || data?.error || text || 'Unknown error';
116
+ const detail = String(
117
+ data?.error?.message || data?.error_description || data?.error || text || 'Unknown error',
118
+ ).slice(0, 2000);
131
119
  throw new Error(`Claude Code OAuth refresh failed: HTTP ${response.status} ${detail}`);
132
120
  }
133
121
  if (!data.access_token) {
@@ -209,8 +197,8 @@ class ClaudeCodeProvider extends AnthropicProvider {
209
197
  });
210
198
  }
211
199
 
212
- async refreshClient() {
213
- const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl);
200
+ async refreshClient(signal = null) {
201
+ const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl, signal);
214
202
  if (!refreshed?.access) return false;
215
203
  this.authToken = refreshed.access;
216
204
  this.refreshToken = refreshed.refresh || this.refreshToken;
@@ -244,7 +232,7 @@ class ClaudeCodeProvider extends AnthropicProvider {
244
232
  if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
245
233
  throw formatClaudeCodeCredentialError(err);
246
234
  }
247
- await this.refreshClient();
235
+ await this.refreshClient(options.signal);
248
236
  try {
249
237
  return await super.chat(messages, tools, options);
250
238
  } catch (retryErr) {
@@ -260,7 +248,7 @@ class ClaudeCodeProvider extends AnthropicProvider {
260
248
  if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
261
249
  throw formatClaudeCodeCredentialError(err);
262
250
  }
263
- await this.refreshClient();
251
+ await this.refreshClient(options.signal);
264
252
  try {
265
253
  yield* super.stream(messages, tools, options);
266
254
  } catch (retryErr) {
@@ -1,4 +1,9 @@
1
1
  const { OpenAIProvider } = require('./openai');
2
+ const {
3
+ fetchResponseText,
4
+ waitForAbortableResult,
5
+ } = require('../../network/http');
6
+ const { sanitizeProviderErrorDetail } = require('./provider_error');
2
7
 
3
8
  class GithubCopilotProvider extends OpenAIProvider {
4
9
  constructor(config = {}) {
@@ -26,35 +31,47 @@ class GithubCopilotProvider extends OpenAIProvider {
26
31
  this._refreshPromise = null;
27
32
  }
28
33
 
29
- async _refreshCopilotToken() {
30
- if (this._refreshPromise) return this._refreshPromise;
31
-
34
+ async _refreshCopilotToken(signal = null) {
32
35
  const now = Math.floor(Date.now() / 1000);
33
36
  // Refresh token if missing or expiring in less than 5 minutes
34
37
  if (this.copilotToken && this.tokenExpiresAt >= now + 300) {
35
38
  return;
36
39
  }
37
40
 
38
- this._refreshPromise = (async () => {
39
- try {
41
+ if (!this._refreshPromise) {
42
+ this._refreshPromise = (async () => {
40
43
  if (!this.githubToken) {
41
44
  throw new Error('GitHub Copilot access token is missing. Please run `neoagent login github-copilot`.');
42
45
  }
43
46
 
44
- const res = await fetch('https://api.github.com/copilot_internal/v2/token', {
47
+ const { response, text } = await fetchResponseText(
48
+ 'https://api.github.com/copilot_internal/v2/token',
49
+ {
45
50
  headers: {
46
51
  'Authorization': `token ${this.githubToken}`,
47
52
  'Accept': 'application/json',
48
53
  'User-Agent': 'NeoAgent/1.0.0'
49
- }
50
- });
51
-
52
- if (!res.ok) {
53
- const errorText = await res.text().catch(() => 'Unknown error');
54
- throw new Error(`Failed to refresh GitHub Copilot token: HTTP ${res.status} - ${errorText}`);
54
+ },
55
+ maxResponseBytes: 1024 * 1024,
56
+ serviceName: 'GitHub Copilot token refresh',
57
+ },
58
+ );
59
+
60
+ if (!response.ok) {
61
+ const error = new Error(
62
+ `Failed to refresh GitHub Copilot token: HTTP ${response.status} - ${sanitizeProviderErrorDetail(text.slice(0, 500))}`,
63
+ );
64
+ error.status = response.status;
65
+ error.headers = response.headers;
66
+ throw error;
55
67
  }
56
68
 
57
- const data = await res.json();
69
+ let data;
70
+ try {
71
+ data = JSON.parse(text || '{}');
72
+ } catch {
73
+ throw new Error('Invalid token response from GitHub Copilot.');
74
+ }
58
75
  if (!data || typeof data.token !== 'string' || !data.token) {
59
76
  throw new Error('Invalid token response from GitHub Copilot.');
60
77
  }
@@ -70,26 +87,30 @@ class GithubCopilotProvider extends OpenAIProvider {
70
87
 
71
88
  // Update the client's API key
72
89
  this.client.apiKey = this.copilotToken;
73
- } finally {
90
+ })().finally(() => {
74
91
  this._refreshPromise = null;
75
- }
76
- })();
92
+ });
93
+ }
77
94
 
78
- return this._refreshPromise;
95
+ return waitForAbortableResult(
96
+ this._refreshPromise,
97
+ signal,
98
+ 'GitHub Copilot token refresh aborted.',
99
+ );
79
100
  }
80
101
 
81
102
  async chat(messages, tools = [], options = {}) {
82
- await this._refreshCopilotToken();
103
+ await this._refreshCopilotToken(options.signal);
83
104
  return super.chat(messages, tools, options);
84
105
  }
85
106
 
86
107
  async *stream(messages, tools = [], options = {}) {
87
- await this._refreshCopilotToken();
108
+ await this._refreshCopilotToken(options.signal);
88
109
  yield* super.stream(messages, tools, options);
89
110
  }
90
111
 
91
112
  async analyzeImage(options = {}) {
92
- await this._refreshCopilotToken();
113
+ await this._refreshCopilotToken(options.signal);
93
114
  return super.analyzeImage(options);
94
115
  }
95
116
  }