newmark-agent 0.3.11 → 0.4.0

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 (69) hide show
  1. package/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +8 -0
  3. package/dist/cli-commands.js +216 -16
  4. package/dist/cli-discovery.d.ts +15 -0
  5. package/dist/cli-discovery.js +182 -0
  6. package/dist/cli-help.d.ts +2 -0
  7. package/dist/cli-help.js +25 -1
  8. package/dist/context/domain/types.d.ts +37 -0
  9. package/dist/context/services/context-orchestrator.js +2 -0
  10. package/dist/conversation-utility-host.bundle.cjs +1503 -214
  11. package/dist/conversation-utility-host.js +3 -0
  12. package/dist/core/agent.d.ts +157 -8
  13. package/dist/core/agent.js +1176 -112
  14. package/dist/core/agentKernel/agent-loop.js +29 -3
  15. package/dist/core/agentKernel/types.d.ts +7 -0
  16. package/dist/core/agentKernelRunner.d.ts +2 -0
  17. package/dist/core/agentKernelRunner.js +174 -27
  18. package/dist/core/config.d.ts +7 -2
  19. package/dist/core/config.js +24 -6
  20. package/dist/core/conversationKernel.d.ts +5 -0
  21. package/dist/core/conversationKernel.js +30 -1
  22. package/dist/core/dshCompatibility.d.ts +198 -0
  23. package/dist/core/dshCompatibility.js +600 -0
  24. package/dist/core/electronUtilityAgentClient.d.ts +4 -0
  25. package/dist/core/electronUtilityAgentClient.js +4 -0
  26. package/dist/core/electronUtilityRuntimePool.d.ts +19 -0
  27. package/dist/core/electronUtilityRuntimePool.js +76 -0
  28. package/dist/core/flow-runner.js +1 -1
  29. package/dist/core/mcpManager.d.ts +1 -0
  30. package/dist/core/mcpManager.js +100 -10
  31. package/dist/core/modelValidationStore.d.ts +4 -1
  32. package/dist/core/modelValidationStore.js +7 -1
  33. package/dist/core/subagent.d.ts +6 -0
  34. package/dist/core/subagent.js +22 -1
  35. package/dist/core/toolPolicy.d.ts +6 -0
  36. package/dist/core/toolPolicy.js +49 -1
  37. package/dist/core/types.d.ts +1 -1
  38. package/dist/core/utilityAgentProtocol.d.ts +8 -1
  39. package/dist/core/workspace.d.ts +15 -0
  40. package/dist/core/workspace.js +62 -1
  41. package/dist/core/wslAgentClient.d.ts +4 -0
  42. package/dist/core/wslAgentClient.js +4 -0
  43. package/dist/core/wslAgentProtocol.d.ts +8 -1
  44. package/dist/core/wslAgentRuntimePool.d.ts +12 -0
  45. package/dist/core/wslAgentRuntimePool.js +71 -0
  46. package/dist/launcher.js +48 -11
  47. package/dist/llm/provider.d.ts +9 -6
  48. package/dist/llm/provider.js +89 -36
  49. package/dist/main.js +326 -52
  50. package/dist/preload.js +17 -0
  51. package/dist/providers/chat-completions.adapter.js +42 -20
  52. package/dist/providers/provider-adapter.d.ts +3 -0
  53. package/dist/providers/provider-events.d.ts +7 -0
  54. package/dist/providers/provider-events.js +44 -0
  55. package/dist/providers/responses.adapter.js +1 -3
  56. package/dist/toolchain/registry/tool-registry.d.ts +13 -1
  57. package/dist/toolchain/registry/tool-registry.js +8 -0
  58. package/dist/toolchain/registry-seeder.js +51 -5
  59. package/dist/tools/index.js +11 -2
  60. package/dist/tools/nativeTools.js +5 -1
  61. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  62. package/dist/tui/src/app.js +47 -13
  63. package/dist/tui/src/render.js +23 -7
  64. package/dist/tui/src/state.js +61 -9
  65. package/dist/ui/index.html +2775 -284
  66. package/dist/ui/lucide-sprite.svg +26 -0
  67. package/dist/wsl-agent-host.bundle.cjs +1503 -214
  68. package/dist/wsl-agent-host.js +3 -0
  69. package/package.json +16 -5
@@ -317,6 +317,10 @@ class WslAgentClient {
317
317
  await this.start();
318
318
  return await this.request('checkpoint', { target: await this.mapTarget(target) }, 5_000);
319
319
  }
320
+ async contextCompress(target, options = {}) {
321
+ await this.start();
322
+ return await this.request('context_compress', { target: await this.mapTarget(target), options }, 120_000);
323
+ }
320
324
  async rateAutoRoute(target, score, routeId = '') {
321
325
  await this.start();
322
326
  return await this.request('rate_auto_route', {
@@ -1,5 +1,5 @@
1
1
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, GuideReceipt } from './types';
2
- import { AgentPromptMessage, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationStopResult } from './conversationKernel';
2
+ import { AgentPromptMessage, ConversationContextCompressOptions, ConversationKernelRunOptions, ConversationKernelRunResult, ConversationQueueMode, ConversationStopResult } from './conversationKernel';
3
3
  import { ConversationRuntimeTarget } from './conversationTarget';
4
4
  import { TerminalTakeoverEvent, TerminalTakeoverOwnerFilter, TerminalTakeoverState } from '../tools/terminalTakeover';
5
5
  import { BrowserUseRequest } from './browserUse';
@@ -114,6 +114,13 @@ export type WslAgentRequest = {
114
114
  params: {
115
115
  target: ConversationRuntimeTarget;
116
116
  };
117
+ } | {
118
+ id: string;
119
+ method: 'context_compress';
120
+ params: {
121
+ target: ConversationRuntimeTarget;
122
+ options?: ConversationContextCompressOptions;
123
+ };
117
124
  } | {
118
125
  id: string;
119
126
  method: 'rate_auto_route';
@@ -11,6 +11,10 @@ export interface WslTargetRuntimeClient {
11
11
  requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslAgentStopResult>;
12
12
  enqueueGuide(target: ConversationRuntimeTarget, envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
13
13
  checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
14
+ contextCompress?(target: ConversationRuntimeTarget, options?: {
15
+ keepRecent?: number;
16
+ force?: boolean;
17
+ }): Promise<Record<string, unknown>>;
14
18
  rateAutoRoute?(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
15
19
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
16
20
  setMode?(target: ConversationRuntimeTarget, mode: AgentMode): Promise<AgentMode>;
@@ -56,6 +60,7 @@ export declare class WslAgentRuntimePool {
56
60
  private hostToolHandler;
57
61
  private restarting;
58
62
  private disposing;
63
+ private forceStopPromises;
59
64
  private capacityTail;
60
65
  private accessSequence;
61
66
  constructor(distro: string, windowsRoot: string, windowsHostScript: string, createClient?: WslTargetRuntimeClientFactory, options?: WslAgentRuntimePoolOptions);
@@ -68,6 +73,10 @@ export declare class WslAgentRuntimePool {
68
73
  requestStop(target: ConversationRuntimeTarget, runId?: string): Promise<WslPoolStopResult>;
69
74
  enqueueGuide(envelope: ConversationInputEnvelope): Promise<GuideReceipt>;
70
75
  checkpoint(target: ConversationRuntimeTarget): Promise<Record<string, unknown>>;
76
+ contextCompress(target: ConversationRuntimeTarget, options?: {
77
+ keepRecent?: number;
78
+ force?: boolean;
79
+ }): Promise<Record<string, unknown>>;
71
80
  rateAutoRoute(target: ConversationRuntimeTarget, score: number, routeId?: string): Promise<WslAutoRouteRatingResult>;
72
81
  setWorkRunExpanded(target: ConversationRuntimeTarget, runId: string, expanded: boolean): Promise<boolean>;
73
82
  setInputMode(target: ConversationRuntimeTarget, mode: string): Promise<'guide' | 'next' | null>;
@@ -90,6 +99,9 @@ export declare class WslAgentRuntimePool {
90
99
  hasActiveWorkspace(target: ConversationRuntimeTarget): Promise<boolean>;
91
100
  stopWorkspace(target: ConversationRuntimeTarget): Promise<void>;
92
101
  stopTarget(target: ConversationRuntimeTarget): Promise<void>;
102
+ /** Hard-stop a target for destructive lifecycle actions such as archive. */
103
+ forceStopTarget(target: ConversationRuntimeTarget): Promise<void>;
104
+ private forceStopTargetInternal;
93
105
  stopAll(): Promise<void>;
94
106
  private throwStopFailures;
95
107
  private stopEntry;
@@ -20,6 +20,7 @@ class WslAgentRuntimePool {
20
20
  hostToolHandler = null;
21
21
  restarting = new Set();
22
22
  disposing = new Set();
23
+ forceStopPromises = new Map();
23
24
  capacityTail = Promise.resolve();
24
25
  accessSequence = 0;
25
26
  constructor(distro, windowsRoot, windowsHostScript, createClient = (target) => new wslAgentClient_1.WslAgentClient(distro, windowsRoot, windowsHostScript, target), options = {}) {
@@ -213,6 +214,21 @@ class WslAgentRuntimePool {
213
214
  this.release(entry, true);
214
215
  }
215
216
  }
217
+ async contextCompress(target, options = {}) {
218
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
219
+ const entry = await this.acquire(normalized);
220
+ try {
221
+ if (entry.stopIntent)
222
+ return { ok: false, error: 'Context compression is unavailable while this conversation is stopping.' };
223
+ if (!entry.client.contextCompress)
224
+ return { ok: false, error: 'Context compression is unavailable in this runtime.' };
225
+ entry.lastSnapshot = null;
226
+ return await entry.client.contextCompress(normalized, options);
227
+ }
228
+ finally {
229
+ this.release(entry, true);
230
+ }
231
+ }
216
232
  async rateAutoRoute(target, score, routeId = '') {
217
233
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
218
234
  const entry = await this.acquireExisting(normalized);
@@ -356,6 +372,61 @@ class WslAgentRuntimePool {
356
372
  if (entry)
357
373
  await this.stopEntry(entry);
358
374
  }
375
+ /** Hard-stop a target for destructive lifecycle actions such as archive. */
376
+ async forceStopTarget(target) {
377
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
378
+ const existing = this.forceStopPromises.get(normalized.runtimeKey);
379
+ if (existing)
380
+ return existing;
381
+ const operation = this.forceStopTargetInternal(normalized);
382
+ this.forceStopPromises.set(normalized.runtimeKey, operation);
383
+ try {
384
+ await operation;
385
+ }
386
+ finally {
387
+ if (this.forceStopPromises.get(normalized.runtimeKey) === operation) {
388
+ this.forceStopPromises.delete(normalized.runtimeKey);
389
+ }
390
+ }
391
+ }
392
+ async forceStopTargetInternal(target) {
393
+ let entry;
394
+ await this.serializeCapacity(async () => {
395
+ entry = this.entries.get(target.runtimeKey);
396
+ if (entry)
397
+ this.disposing.add(target.runtimeKey);
398
+ });
399
+ if (!entry)
400
+ return;
401
+ const intent = entry.stopIntent || {
402
+ runId: entry.lastRunId,
403
+ generation: entry.lastGeneration,
404
+ checkpointed: false,
405
+ forcePromise: null,
406
+ };
407
+ if (!entry.stopIntent)
408
+ entry.stopIntent = intent;
409
+ try {
410
+ await this.forceTerminateEntry(entry, intent);
411
+ if (entry.client.status().connected) {
412
+ if (entry.client.forceStopRuntimeGroup)
413
+ await entry.client.forceStopRuntimeGroup();
414
+ else
415
+ await entry.client.forceRestartRuntimeGroup();
416
+ }
417
+ if (entry.client.status().connected) {
418
+ throw new Error(`WSL runtime ${target.runtimeKey} remained connected after force stop`);
419
+ }
420
+ if (this.entries.get(target.runtimeKey) === entry) {
421
+ this.entries.delete(target.runtimeKey);
422
+ entry.unsubscribe();
423
+ entry.client.setHostToolHandler(null);
424
+ }
425
+ }
426
+ finally {
427
+ this.disposing.delete(target.runtimeKey);
428
+ }
429
+ }
359
430
  async stopAll() {
360
431
  const targets = Array.from(this.entries.values(), entry => entry.target);
361
432
  const results = await Promise.allSettled(targets.map(target => this.stopTarget(target)));
package/dist/launcher.js CHANGED
@@ -43,19 +43,35 @@ const agent_1 = require("./core/agent");
43
43
  const flow_1 = require("./core/flow");
44
44
  const flow_runner_1 = require("./core/flow-runner");
45
45
  const cli_commands_1 = require("./cli-commands");
46
+ const cli_discovery_1 = require("./cli-discovery");
46
47
  const installUpdate_1 = require("./core/installUpdate");
47
48
  const cli_help_1 = require("./cli-help");
48
- const args = process.argv.slice(2);
49
+ const rawArgs = process.argv.slice(2);
50
+ const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
49
51
  const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
52
+ const cliCommand = args.find(a => cli_commands_1.CLI_COMMANDS.includes(a));
53
+ const isEdit = args[0] === 'edit';
54
+ const editFile = isEdit ? args[1] : '';
55
+ const isFlow = args[0] === 'flow';
50
56
  const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
51
- const isVersionArg = !hasCliCommand && args.some(arg => ['--version', '-v'].includes(arg.toLowerCase()));
57
+ const isVersionArg = !hasCliCommand && (0, cli_discovery_1.isVersionArgument)(args);
58
+ const isReadOnlyValidation = hasCliCommand && args.includes('validate-models') && !args.includes('--persist');
52
59
  const isTui = args.some(arg => arg.toLowerCase() === '--tui');
53
60
  const isGui = args.some(arg => arg.toLowerCase() === '--gui');
54
61
  const isCli = args.includes('--cli');
55
62
  const isServer = args.includes('--server');
56
- const isEdit = args[0] === 'edit';
57
- const editFile = isEdit ? args[1] : '';
58
- const isFlow = args[0] === 'flow';
63
+ const invalidArgument = (0, cli_discovery_1.invalidTopLevelArgument)(args);
64
+ if (invalidArgument) {
65
+ console.error(`Invalid Newmark argument: ${invalidArgument}`);
66
+ process.exit(2);
67
+ }
68
+ // Command help is a terminating, read-only discovery operation. Resolve it
69
+ // before first-run initialization or GUI forwarding so `Newmark.exe send
70
+ // --help` cannot accidentally enter Electron or instantiate an Agent.
71
+ if (hasCliCommand && cliCommand && (0, cli_commands_1.cliHelpRequested)(args)) {
72
+ console.log((0, cli_commands_1.cliCommandHelp)(cliCommand));
73
+ process.exit(0);
74
+ }
59
75
  function pathArgValue(values, key) {
60
76
  const prefix = `${key}=`;
61
77
  const inlineIdx = values.findIndex(a => a.startsWith(prefix));
@@ -89,6 +105,15 @@ function pathArgValue(values, key) {
89
105
  }
90
106
  return best || parts.join(' ') || undefined;
91
107
  }
108
+ function resolveTuiWorkspacePath(values, root) {
109
+ const explicitWorkspace = pathArgValue(values, '--workspace');
110
+ if (explicitWorkspace)
111
+ return explicitWorkspace;
112
+ // An explicitly isolated runtime must not silently register the caller's
113
+ // cwd as an external workspace. Keep the opt-in --workspace escape hatch,
114
+ // while making the safe one-argument form fully self-contained.
115
+ return pathArgValue(values, '--root') ? root : process.cwd();
116
+ }
92
117
  function userRuntimeRoot() {
93
118
  return path.join(os.homedir(), '.Newmark');
94
119
  }
@@ -174,18 +199,30 @@ function writableRuntimeRoot(candidate) {
174
199
  const explicitRoot = pathArgValue(args, '--root');
175
200
  const root = explicitRoot ? writableRuntimeRoot(explicitRoot) : userRuntimeRoot();
176
201
  if (isHelpArg) {
177
- console.log((0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
202
+ console.log(isFlow ? (0, cli_help_1.newmarkFlowHelpText)() : isEdit ? (0, cli_help_1.newmarkEditHelpText)() : (0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
178
203
  process.exit(0);
179
204
  }
180
205
  if (isVersionArg) {
181
206
  console.log((0, installUpdate_1.currentAppVersion)());
182
207
  process.exit(0);
183
208
  }
184
- function firstRunInit(r) {
209
+ const unknownCommand = !hasCliCommand && !isTui && !isGui && !isCli && !isServer && !isFlow && !isEdit
210
+ ? (0, cli_discovery_1.unknownTopLevelCommand)(args)
211
+ : undefined;
212
+ if (unknownCommand) {
213
+ console.error(`Unknown Newmark command or argument: ${unknownCommand}. Run --help to see the supported entrypoints.`);
214
+ process.exit(2);
215
+ }
216
+ function firstRunInit(r, options = {}) {
185
217
  fs.mkdirSync(r, { recursive: true });
186
- migrateLegacyRuntimeRoot(r);
218
+ // Legacy AppData migration is only valid for the canonical user runtime.
219
+ // An explicit --root is an isolation boundary (tests, portable roots, and
220
+ // caller-selected workspaces) and must never inherit the user's sessions,
221
+ // archives, providers, or workspaces.
222
+ if (path.resolve(r) === path.resolve(userRuntimeRoot()))
223
+ migrateLegacyRuntimeRoot(r);
187
224
  const { ensureRootConfig } = require('./core/config');
188
- ensureRootConfig(r);
225
+ ensureRootConfig(r, options);
189
226
  if (!fs.existsSync(path.join(r, 'agent.md'))) {
190
227
  fs.writeFileSync(path.join(r, 'agent.md'), '# Newmark Agent\n\nYou are a powerful coding assistant.\n', 'utf-8');
191
228
  }
@@ -270,13 +307,13 @@ function launchGui() {
270
307
  console.error('Unable to locate the Newmark GUI runtime. Reinstall newmark-agent with optional dependencies enabled, or install a Newmark desktop package.');
271
308
  process.exit(1);
272
309
  }
273
- firstRunInit(root);
310
+ firstRunInit(root, { readOnly: isReadOnlyValidation });
274
311
  if (isGui) {
275
312
  launchGui();
276
313
  }
277
314
  else if (isTui) {
278
315
  const { start } = require('./tui/src/app');
279
- start({ root, workspacePath: process.cwd(), desktopDist: __dirname });
316
+ start({ root, workspacePath: resolveTuiWorkspacePath(args, root), desktopDist: __dirname });
280
317
  }
281
318
  else if (hasCliCommand) {
282
319
  (0, cli_commands_1.runCliCommand)(root, args).then(handled => {
@@ -28,9 +28,12 @@ export declare class LLMProvider {
28
28
  explicitProtocol?: ProviderProtocol | undefined;
29
29
  openAIMode: OpenAITransportMode | boolean;
30
30
  useProviderAdaptersV2: boolean;
31
+ requestTimeoutMs: number;
31
32
  static nodeHttpTransport: ((method: 'GET' | 'POST', url: string, headers: Record<string, string>, body?: string) => Promise<NodeHttpResult>) | null;
32
33
  static powershellTransport: ((method: 'GET' | 'POST', url: string, headers: Record<string, string>, body?: string) => Promise<NodeHttpResult>) | null;
33
- constructor(name: string, baseUrl: string, apiKey: string, explicitProtocol?: ProviderProtocol | undefined, openAIMode?: OpenAITransportMode | boolean, useProviderAdaptersV2?: boolean);
34
+ constructor(name: string, baseUrl: string, apiKey: string, explicitProtocol?: ProviderProtocol | undefined, openAIMode?: OpenAITransportMode | boolean, useProviderAdaptersV2?: boolean, requestTimeoutMs?: number);
35
+ private effectiveRequestTimeout;
36
+ private withRequestTimeout;
34
37
  intelligenceConfig(tier: string): IntelligenceConfig;
35
38
  private reasoningEffort;
36
39
  private applyChatReasoningEffort;
@@ -85,16 +88,16 @@ export declare class LLMProvider {
85
88
  private toStreamUsage;
86
89
  private shouldDowngradeToResponses;
87
90
  /**
88
- * Loopback-aware transport injected into adapter `execute`. Mirrors the
89
- * legacy orchestration exactly: streaming requests go through fetch with a
90
- * 120s timeout and degrade to a non-streaming node-http request on fetch
91
- * failure; non-streaming requests reuse postJsonWithFetchFallback.
91
+ * Loopback-aware transport injected into adapter `execute`. Streaming
92
+ * requests retain the fetch-to-node fallback for transport failures, while
93
+ * a local deadline is returned directly so one request cannot become a
94
+ * second Windows fallback request.
92
95
  */
93
96
  private buildProviderAdapterTransport;
94
97
  private toTransportResponse;
95
98
  private toNormalizedMessages;
96
99
  private toNormalizedTools;
97
- chatStreamWithTools(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, tools: unknown[], signal?: AbortSignal, reasoningTier?: string): AsyncGenerator<StreamToken>;
100
+ chatStreamWithTools(model: string, messages: Array<Record<string, unknown>>, systemPrompt: string | null, temperature: number, maxTokens: number, tools: unknown[], signal?: AbortSignal, reasoningTier?: string, sessionId?: string): AsyncGenerator<StreamToken>;
98
101
  /**
99
102
  * GitHub Models streaming path. Preserved as a dedicated implementation
100
103
  * because the provider adapters (V2) do not serialize the GitHub Models
@@ -43,6 +43,19 @@ const child_process_1 = require("child_process");
43
43
  const agentKernelDiagnostics_1 = require("../core/agentKernelDiagnostics");
44
44
  const chat_messages_1 = require("../providers/chat-messages");
45
45
  const providers_1 = require("../providers");
46
+ // Keep provider requests below the release-harness/user-visible command
47
+ // deadline. A provider that does not answer must produce one bounded error;
48
+ // it must not restart the same request through every Windows transport.
49
+ const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 90_000;
50
+ const MIN_PROVIDER_REQUEST_TIMEOUT_MS = 50;
51
+ function providerTimeoutError(timeoutMs) {
52
+ const error = new Error(`Provider request timed out after ${timeoutMs}ms`);
53
+ error.name = 'TimeoutError';
54
+ return error;
55
+ }
56
+ function isProviderTimeoutError(error) {
57
+ return error instanceof Error && error.name === 'TimeoutError';
58
+ }
46
59
  function abortFailure(signal) {
47
60
  const reason = signal?.reason;
48
61
  const error = reason instanceof Error ? reason : new Error(reason ? String(reason) : 'LLM request aborted');
@@ -84,15 +97,37 @@ class LLMProvider {
84
97
  explicitProtocol;
85
98
  openAIMode;
86
99
  useProviderAdaptersV2;
100
+ requestTimeoutMs;
87
101
  static nodeHttpTransport = null;
88
102
  static powershellTransport = null;
89
- constructor(name, baseUrl, apiKey, explicitProtocol, openAIMode = 'chat_stream', useProviderAdaptersV2 = false) {
103
+ constructor(name, baseUrl, apiKey, explicitProtocol, openAIMode = 'chat_stream', useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
90
104
  this.name = name;
91
105
  this.baseUrl = baseUrl;
92
106
  this.apiKey = apiKey;
93
107
  this.explicitProtocol = explicitProtocol;
94
108
  this.openAIMode = openAIMode;
95
109
  this.useProviderAdaptersV2 = useProviderAdaptersV2;
110
+ this.requestTimeoutMs = requestTimeoutMs;
111
+ }
112
+ effectiveRequestTimeout(timeoutMs) {
113
+ const requested = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
114
+ const configured = Number.isFinite(this.requestTimeoutMs) && this.requestTimeoutMs > 0
115
+ ? this.requestTimeoutMs
116
+ : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
117
+ return Math.max(MIN_PROVIDER_REQUEST_TIMEOUT_MS, Math.min(requested, configured));
118
+ }
119
+ async withRequestTimeout(promise, timeoutMs, signal) {
120
+ let timer;
121
+ const timeoutPromise = new Promise((_, reject) => {
122
+ timer = setTimeout(() => reject(providerTimeoutError(timeoutMs)), timeoutMs);
123
+ });
124
+ try {
125
+ return await abortable(Promise.race([promise, timeoutPromise]), signal);
126
+ }
127
+ finally {
128
+ if (timer)
129
+ clearTimeout(timer);
130
+ }
96
131
  }
97
132
  intelligenceConfig(tier) {
98
133
  switch (tier) {
@@ -212,6 +247,7 @@ class LLMProvider {
212
247
  };
213
248
  }
214
249
  async postJsonWithFetchFallback(url, headers, body, timeoutMs = 120000, signal) {
250
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
215
251
  // Electron utility processes can leave an undici response body pending when
216
252
  // several isolated workers concurrently call a plain-HTTP local provider.
217
253
  // Node's HTTP client owns the full body lifecycle and is deterministic for
@@ -224,7 +260,7 @@ class LLMProvider {
224
260
  return '';
225
261
  } })();
226
262
  this.transportDiagnostic('loopback:start', pathname);
227
- const local = await this.nodeHttpJson('POST', url, headers, JSON.stringify(body), signal);
263
+ const local = await this.nodeHttpJson('POST', url, headers, JSON.stringify(body), signal, effectiveTimeout);
228
264
  this.transportDiagnostic('loopback:complete', `status=${local.status} bytes=${Buffer.byteLength(local.body || '')}`);
229
265
  return {
230
266
  ok: local.status >= 200 && local.status < 300,
@@ -240,7 +276,7 @@ class LLMProvider {
240
276
  forwardAbort();
241
277
  else
242
278
  signal?.addEventListener('abort', forwardAbort, { once: true });
243
- const timer = setTimeout(() => abort.abort(), timeoutMs);
279
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
244
280
  try {
245
281
  const response = await fetch(url, {
246
282
  method: 'POST',
@@ -253,9 +289,11 @@ class LLMProvider {
253
289
  catch (e) {
254
290
  if (signal?.aborted)
255
291
  throw abortFailure(signal);
292
+ if (abort.signal.aborted)
293
+ throw abortFailure(abort.signal);
256
294
  if (!this.shouldUseNodeHttpFallback(e))
257
295
  throw e;
258
- const fallback = await this.nodeHttpJson('POST', url, headers, JSON.stringify(body), signal);
296
+ const fallback = await this.nodeHttpJson('POST', url, headers, JSON.stringify(body), signal, effectiveTimeout);
259
297
  return {
260
298
  ok: fallback.status >= 200 && fallback.status < 300,
261
299
  status: fallback.status,
@@ -270,16 +308,19 @@ class LLMProvider {
270
308
  }
271
309
  }
272
310
  async getJsonWithFetchFallback(url, headers, timeoutMs = 30000) {
311
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
273
312
  const abort = new AbortController();
274
- const timer = setTimeout(() => abort.abort(), timeoutMs);
313
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
275
314
  try {
276
315
  const response = await fetch(url, { method: 'GET', headers, signal: abort.signal });
277
316
  return response;
278
317
  }
279
318
  catch (e) {
319
+ if (abort.signal.aborted)
320
+ throw abortFailure(abort.signal);
280
321
  if (!this.shouldUseNodeHttpFallback(e))
281
322
  throw e;
282
- const fallback = await this.nodeHttpJson('GET', url, headers);
323
+ const fallback = await this.nodeHttpJson('GET', url, headers, '', undefined, effectiveTimeout);
283
324
  return {
284
325
  ok: fallback.status >= 200 && fallback.status < 300,
285
326
  status: fallback.status,
@@ -293,16 +334,21 @@ class LLMProvider {
293
334
  }
294
335
  }
295
336
  shouldUseNodeHttpFallback(error) {
296
- return ((error instanceof TypeError && /fetch failed/i.test(error.message)) ||
297
- (error instanceof Error && /abort/i.test(error.name || error.message)));
337
+ // Abort is a completed control decision (user cancellation or our own
338
+ // deadline), not evidence that a second transport can succeed. Retrying it
339
+ // on Windows used to create a second 120s request after the first timeout.
340
+ return error instanceof TypeError && /fetch failed/i.test(error.message);
298
341
  }
299
- nodeHttpJson(method, urlValue, headers, body = '', signal) {
342
+ nodeHttpJson(method, urlValue, headers, body = '', signal, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
343
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
300
344
  if (LLMProvider.nodeHttpTransport) {
301
- return abortable(LLMProvider.nodeHttpTransport(method, urlValue, headers, body), signal).catch(error => {
345
+ return this.withRequestTimeout(LLMProvider.nodeHttpTransport(method, urlValue, headers, body), effectiveTimeout, signal).catch(error => {
302
346
  if (signal?.aborted)
303
347
  throw abortFailure(signal);
348
+ if (isProviderTimeoutError(error))
349
+ throw error;
304
350
  if (process.platform === 'win32') {
305
- return this.powershellJson(method, urlValue, headers, body, signal);
351
+ return this.powershellJson(method, urlValue, headers, body, signal, effectiveTimeout);
306
352
  }
307
353
  throw error;
308
354
  });
@@ -350,8 +396,8 @@ class LLMProvider {
350
396
  fail(new Error('Node HTTP response closed before completion'));
351
397
  });
352
398
  });
353
- req.setTimeout(120000, () => {
354
- req.destroy(new Error('Node HTTP fallback timeout'));
399
+ req.setTimeout(effectiveTimeout, () => {
400
+ req.destroy(providerTimeoutError(effectiveTimeout));
355
401
  });
356
402
  req.on('error', reject);
357
403
  const onAbort = () => req.destroy(abortFailure(signal));
@@ -366,15 +412,18 @@ class LLMProvider {
366
412
  }).catch(error => {
367
413
  if (signal?.aborted)
368
414
  throw abortFailure(signal);
415
+ if (isProviderTimeoutError(error))
416
+ throw error;
369
417
  if (process.platform === 'win32') {
370
- return this.powershellJson(method, urlValue, headers, body, signal);
418
+ return this.powershellJson(method, urlValue, headers, body, signal, effectiveTimeout);
371
419
  }
372
420
  throw error;
373
421
  });
374
422
  }
375
- powershellJson(method, urlValue, headers, body = '', signal) {
423
+ powershellJson(method, urlValue, headers, body = '', signal, timeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
424
+ const effectiveTimeout = this.effectiveRequestTimeout(timeoutMs);
376
425
  if (LLMProvider.powershellTransport) {
377
- return LLMProvider.powershellTransport(method, urlValue, headers, body);
426
+ return this.withRequestTimeout(LLMProvider.powershellTransport(method, urlValue, headers, body), effectiveTimeout, signal);
378
427
  }
379
428
  return new Promise((resolve, reject) => {
380
429
  const headerJson = JSON.stringify(headers);
@@ -403,7 +452,7 @@ class LLMProvider {
403
452
  ' $raw = $headerJson | ConvertFrom-Json',
404
453
  ' foreach ($p in $raw.PSObject.Properties) { $headers[$p.Name] = [string]$p.Value }',
405
454
  '}',
406
- '$params = @{ Uri = $uri; Method = $method; Headers = $headers; UseBasicParsing = $true; TimeoutSec = 120 }',
455
+ `'$params = @{ Uri = $uri; Method = $method; Headers = $headers; UseBasicParsing = $true; TimeoutSec = ${Math.max(1, Math.ceil(effectiveTimeout / 1000))} }`,
407
456
  'if ($method -eq "POST") { $params["Body"] = $bodyJson }',
408
457
  'if ($method -eq "POST") { $params["ContentType"] = "application/json; charset=utf-8" }',
409
458
  '$resp = Invoke-WebRequest @params',
@@ -437,8 +486,8 @@ class LLMProvider {
437
486
  const timer = setTimeout(() => {
438
487
  child.kill();
439
488
  cleanup();
440
- reject(new Error('PowerShell HTTP fallback timeout'));
441
- }, 130000);
489
+ reject(providerTimeoutError(effectiveTimeout));
490
+ }, effectiveTimeout + 5000);
442
491
  child.stdout.setEncoding('utf8');
443
492
  child.stderr.setEncoding('utf8');
444
493
  child.stdout.on('data', chunk => { stdout += chunk; });
@@ -794,7 +843,7 @@ class LLMProvider {
794
843
  * The emitted request body and StreamToken stream are byte-equivalent to
795
844
  * the legacy inlined path.
796
845
  */
797
- async *chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier) {
846
+ async *chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId) {
798
847
  const mode = this.openAITransportMode();
799
848
  if (mode === 'responses') {
800
849
  yield* this.adapterResponsesBridge(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier);
@@ -812,6 +861,7 @@ class LLMProvider {
812
861
  maxOutputTokens: maxTokens,
813
862
  apiKey: this.apiKey,
814
863
  baseUrl: this.cleanBaseUrl(),
864
+ ...(sessionId ? { sessionId } : {}),
815
865
  };
816
866
  const serialized = await adapter.serializeRequest(request);
817
867
  serialized.body.stream = mode === 'chat' ? false : true;
@@ -926,10 +976,10 @@ class LLMProvider {
926
976
  return this.shouldUseResponsesFallback(Number(match[1]), errorText);
927
977
  }
928
978
  /**
929
- * Loopback-aware transport injected into adapter `execute`. Mirrors the
930
- * legacy orchestration exactly: streaming requests go through fetch with a
931
- * 120s timeout and degrade to a non-streaming node-http request on fetch
932
- * failure; non-streaming requests reuse postJsonWithFetchFallback.
979
+ * Loopback-aware transport injected into adapter `execute`. Streaming
980
+ * requests retain the fetch-to-node fallback for transport failures, while
981
+ * a local deadline is returned directly so one request cannot become a
982
+ * second Windows fallback request.
933
983
  */
934
984
  buildProviderAdapterTransport() {
935
985
  return async (request, signal) => {
@@ -940,7 +990,8 @@ class LLMProvider {
940
990
  forwardAbort();
941
991
  else
942
992
  signal?.addEventListener('abort', forwardAbort, { once: true });
943
- const timer = setTimeout(() => abort.abort(), 120000);
993
+ const effectiveTimeout = this.effectiveRequestTimeout(120000);
994
+ const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
944
995
  try {
945
996
  try {
946
997
  return await fetch(request.url, {
@@ -953,11 +1004,13 @@ class LLMProvider {
953
1004
  catch (error) {
954
1005
  if (signal?.aborted)
955
1006
  throw abortFailure(signal);
1007
+ if (abort.signal.aborted)
1008
+ throw abortFailure(abort.signal);
956
1009
  if (!this.shouldUseNodeHttpFallback(error))
957
1010
  throw error;
958
1011
  const fallbackHeaders = { ...request.headers };
959
1012
  delete fallbackHeaders['Accept'];
960
- const fallback = await this.postJsonWithFetchFallback(request.url, fallbackHeaders, { ...request.body, stream: false }, 120000, signal);
1013
+ const fallback = await this.postJsonWithFetchFallback(request.url, fallbackHeaders, { ...request.body, stream: false }, effectiveTimeout, signal);
961
1014
  return this.toTransportResponse(fallback);
962
1015
  }
963
1016
  }
@@ -1028,7 +1081,7 @@ class LLMProvider {
1028
1081
  };
1029
1082
  });
1030
1083
  }
1031
- async *chatStreamWithTools(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier) {
1084
+ async *chatStreamWithTools(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId) {
1032
1085
  if (signal?.aborted)
1033
1086
  throw abortFailure(signal);
1034
1087
  if (this.protocol() === 'anthropic') {
@@ -1040,7 +1093,7 @@ class LLMProvider {
1040
1093
  return;
1041
1094
  }
1042
1095
  if (this.useProviderAdaptersV2) {
1043
- yield* this.chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier);
1096
+ yield* this.chatStreamWithToolsV2(model, messages, systemPrompt, temperature, maxTokens, tools, signal, reasoningTier, sessionId);
1044
1097
  return;
1045
1098
  }
1046
1099
  throw new Error('LLMProvider legacy OpenAI streaming was removed in dev-0.3.0: enable provider_adapters_v2 (useProviderAdaptersV2).');
@@ -1071,7 +1124,8 @@ class LLMProvider {
1071
1124
  forwardAbort();
1072
1125
  else
1073
1126
  signal?.addEventListener('abort', forwardAbort, { once: true });
1074
- const timeout = setTimeout(() => abort.abort(), 120000);
1127
+ const effectiveTimeout = this.effectiveRequestTimeout(120000);
1128
+ const timeout = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
1075
1129
  let reader = null;
1076
1130
  try {
1077
1131
  let response;
@@ -1084,11 +1138,13 @@ class LLMProvider {
1084
1138
  });
1085
1139
  }
1086
1140
  catch (e) {
1141
+ if (signal?.aborted)
1142
+ throw abortFailure(signal);
1143
+ if (abort.signal.aborted)
1144
+ throw abortFailure(abort.signal);
1087
1145
  if (!this.shouldUseNodeHttpFallback(e))
1088
1146
  throw e;
1089
1147
  clearTimeout(timeout);
1090
- if (signal?.aborted)
1091
- throw abortFailure(signal);
1092
1148
  yield* this.githubModelsChatNonStreaming(url, body, signal);
1093
1149
  return;
1094
1150
  }
@@ -1108,12 +1164,9 @@ class LLMProvider {
1108
1164
  let currentReasoningContent = '';
1109
1165
  let contentPolicyBlocked = false;
1110
1166
  let emittedContent = false;
1167
+ const streamSignal = signal || new AbortController().signal;
1111
1168
  while (true) {
1112
- if (signal?.aborted)
1113
- throw abortFailure(signal);
1114
- const readPromise = reader.read();
1115
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Stream read timeout')), 30000));
1116
- const { done, value } = await Promise.race([readPromise, timeoutPromise]);
1169
+ const { done, value } = await (0, providers_1.readProviderStreamChunk)(reader, streamSignal);
1117
1170
  if (done)
1118
1171
  break;
1119
1172
  buffer += decoder.decode(value, { stream: true });