newmark-agent 0.3.11 → 0.3.12
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/config.example.json +6 -0
- package/dist/cli-commands.d.ts +7 -0
- package/dist/cli-commands.js +206 -15
- package/dist/cli-discovery.d.ts +15 -0
- package/dist/cli-discovery.js +182 -0
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.js +25 -1
- package/dist/conversation-utility-host.bundle.cjs +357 -84
- package/dist/core/agent.d.ts +18 -3
- package/dist/core/agent.js +214 -30
- package/dist/core/agentKernelRunner.js +53 -8
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.js +24 -6
- package/dist/core/conversationKernel.js +1 -1
- package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
- package/dist/core/electronUtilityRuntimePool.js +62 -0
- package/dist/core/flow-runner.js +1 -1
- package/dist/core/modelValidationStore.d.ts +4 -1
- package/dist/core/modelValidationStore.js +7 -1
- package/dist/core/workspace.d.ts +6 -0
- package/dist/core/workspace.js +14 -0
- package/dist/core/wslAgentRuntimePool.d.ts +4 -0
- package/dist/core/wslAgentRuntimePool.js +56 -0
- package/dist/launcher.js +40 -11
- package/dist/llm/provider.d.ts +8 -5
- package/dist/llm/provider.js +85 -33
- package/dist/main.js +167 -45
- package/dist/preload.js +6 -0
- package/dist/providers/chat-completions.adapter.js +1 -5
- package/dist/providers/provider-events.d.ts +7 -0
- package/dist/providers/provider-events.js +44 -0
- package/dist/providers/responses.adapter.js +1 -3
- package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
- package/dist/tui/src/app.js +47 -13
- package/dist/tui/src/render.js +23 -7
- package/dist/tui/src/state.js +61 -9
- package/dist/ui/index.html +250 -84
- package/dist/wsl-agent-host.bundle.cjs +357 -84
- package/package.json +14 -5
package/dist/llm/provider.js
CHANGED
|
@@ -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(),
|
|
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(),
|
|
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
|
-
|
|
297
|
-
|
|
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
|
|
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(
|
|
354
|
-
req.destroy(
|
|
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 =
|
|
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(
|
|
441
|
-
},
|
|
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; });
|
|
@@ -926,10 +975,10 @@ class LLMProvider {
|
|
|
926
975
|
return this.shouldUseResponsesFallback(Number(match[1]), errorText);
|
|
927
976
|
}
|
|
928
977
|
/**
|
|
929
|
-
* Loopback-aware transport injected into adapter `execute`.
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
978
|
+
* Loopback-aware transport injected into adapter `execute`. Streaming
|
|
979
|
+
* requests retain the fetch-to-node fallback for transport failures, while
|
|
980
|
+
* a local deadline is returned directly so one request cannot become a
|
|
981
|
+
* second Windows fallback request.
|
|
933
982
|
*/
|
|
934
983
|
buildProviderAdapterTransport() {
|
|
935
984
|
return async (request, signal) => {
|
|
@@ -940,7 +989,8 @@ class LLMProvider {
|
|
|
940
989
|
forwardAbort();
|
|
941
990
|
else
|
|
942
991
|
signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
943
|
-
const
|
|
992
|
+
const effectiveTimeout = this.effectiveRequestTimeout(120000);
|
|
993
|
+
const timer = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
|
|
944
994
|
try {
|
|
945
995
|
try {
|
|
946
996
|
return await fetch(request.url, {
|
|
@@ -953,11 +1003,13 @@ class LLMProvider {
|
|
|
953
1003
|
catch (error) {
|
|
954
1004
|
if (signal?.aborted)
|
|
955
1005
|
throw abortFailure(signal);
|
|
1006
|
+
if (abort.signal.aborted)
|
|
1007
|
+
throw abortFailure(abort.signal);
|
|
956
1008
|
if (!this.shouldUseNodeHttpFallback(error))
|
|
957
1009
|
throw error;
|
|
958
1010
|
const fallbackHeaders = { ...request.headers };
|
|
959
1011
|
delete fallbackHeaders['Accept'];
|
|
960
|
-
const fallback = await this.postJsonWithFetchFallback(request.url, fallbackHeaders, { ...request.body, stream: false },
|
|
1012
|
+
const fallback = await this.postJsonWithFetchFallback(request.url, fallbackHeaders, { ...request.body, stream: false }, effectiveTimeout, signal);
|
|
961
1013
|
return this.toTransportResponse(fallback);
|
|
962
1014
|
}
|
|
963
1015
|
}
|
|
@@ -1071,7 +1123,8 @@ class LLMProvider {
|
|
|
1071
1123
|
forwardAbort();
|
|
1072
1124
|
else
|
|
1073
1125
|
signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
1074
|
-
const
|
|
1126
|
+
const effectiveTimeout = this.effectiveRequestTimeout(120000);
|
|
1127
|
+
const timeout = setTimeout(() => abort.abort(providerTimeoutError(effectiveTimeout)), effectiveTimeout);
|
|
1075
1128
|
let reader = null;
|
|
1076
1129
|
try {
|
|
1077
1130
|
let response;
|
|
@@ -1084,11 +1137,13 @@ class LLMProvider {
|
|
|
1084
1137
|
});
|
|
1085
1138
|
}
|
|
1086
1139
|
catch (e) {
|
|
1140
|
+
if (signal?.aborted)
|
|
1141
|
+
throw abortFailure(signal);
|
|
1142
|
+
if (abort.signal.aborted)
|
|
1143
|
+
throw abortFailure(abort.signal);
|
|
1087
1144
|
if (!this.shouldUseNodeHttpFallback(e))
|
|
1088
1145
|
throw e;
|
|
1089
1146
|
clearTimeout(timeout);
|
|
1090
|
-
if (signal?.aborted)
|
|
1091
|
-
throw abortFailure(signal);
|
|
1092
1147
|
yield* this.githubModelsChatNonStreaming(url, body, signal);
|
|
1093
1148
|
return;
|
|
1094
1149
|
}
|
|
@@ -1108,12 +1163,9 @@ class LLMProvider {
|
|
|
1108
1163
|
let currentReasoningContent = '';
|
|
1109
1164
|
let contentPolicyBlocked = false;
|
|
1110
1165
|
let emittedContent = false;
|
|
1166
|
+
const streamSignal = signal || new AbortController().signal;
|
|
1111
1167
|
while (true) {
|
|
1112
|
-
|
|
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]);
|
|
1168
|
+
const { done, value } = await (0, providers_1.readProviderStreamChunk)(reader, streamSignal);
|
|
1117
1169
|
if (done)
|
|
1118
1170
|
break;
|
|
1119
1171
|
buffer += decoder.decode(value, { stream: true });
|
package/dist/main.js
CHANGED
|
@@ -51,6 +51,7 @@ const electronBrowserUseHost_1 = require("./core/electronBrowserUseHost");
|
|
|
51
51
|
const flow_1 = require("./core/flow");
|
|
52
52
|
const flow_runner_1 = require("./core/flow-runner");
|
|
53
53
|
const cli_commands_1 = require("./cli-commands");
|
|
54
|
+
const cli_discovery_1 = require("./cli-discovery");
|
|
54
55
|
const config_1 = require("./core/config");
|
|
55
56
|
const memoryLab_1 = require("./core/memoryLab");
|
|
56
57
|
const installUpdate_1 = require("./core/installUpdate");
|
|
@@ -426,7 +427,11 @@ function createAppIconImage(size) {
|
|
|
426
427
|
return size ? icon.resize({ width: size, height: size }) : icon;
|
|
427
428
|
}
|
|
428
429
|
function userArgs() {
|
|
429
|
-
|
|
430
|
+
const args = process.argv.slice(1);
|
|
431
|
+
// The native Windows console wrapper inserts Electron's `--` boundary before
|
|
432
|
+
// user arguments. Electron leaves that boundary in process.argv; normalize it
|
|
433
|
+
// before command discovery so the literal `Newmark.exe help` path terminates.
|
|
434
|
+
return args[0] === '--' ? args.slice(1) : args;
|
|
430
435
|
}
|
|
431
436
|
function argValue(args, key) {
|
|
432
437
|
const idx = args.indexOf(key);
|
|
@@ -486,14 +491,14 @@ function positionalAfter(args, commandName) {
|
|
|
486
491
|
return values;
|
|
487
492
|
}
|
|
488
493
|
// First-run initialization
|
|
489
|
-
function firstRunInit(root) {
|
|
494
|
+
function firstRunInit(root, options = {}) {
|
|
490
495
|
fs.mkdirSync(root, { recursive: true });
|
|
491
496
|
for (const d of ['skills', 'Work', 'Flow', 'archive', 'Memory Lab']) {
|
|
492
497
|
fs.mkdirSync(path.join(root, d), { recursive: true });
|
|
493
498
|
}
|
|
494
499
|
new memoryLab_1.MemoryLabManager(root);
|
|
495
500
|
const configModule = require('./core/config');
|
|
496
|
-
configModule.ensureRootConfig(root);
|
|
501
|
+
configModule.ensureRootConfig(root, options);
|
|
497
502
|
if (!fs.existsSync(path.join(root, 'agent.md'))) {
|
|
498
503
|
fs.writeFileSync(path.join(root, 'agent.md'), '# Newmark Agent\n\nYou are a powerful coding assistant.\n', 'utf-8');
|
|
499
504
|
}
|
|
@@ -583,18 +588,13 @@ function ensureElectronUtilityRuntimeHost() {
|
|
|
583
588
|
return host;
|
|
584
589
|
}
|
|
585
590
|
function legacyUserDataRoot() {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
catch {
|
|
590
|
-
if (process.platform === 'win32') {
|
|
591
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
592
|
-
return path.join(appData, 'Newmark Agent');
|
|
593
|
-
}
|
|
594
|
-
if (process.platform === 'darwin')
|
|
595
|
-
return path.join(os.homedir(), 'Library', 'Application Support', 'Newmark Agent');
|
|
596
|
-
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Newmark Agent');
|
|
591
|
+
if (process.platform === 'win32') {
|
|
592
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
593
|
+
return path.join(appData, 'Newmark Agent');
|
|
597
594
|
}
|
|
595
|
+
if (process.platform === 'darwin')
|
|
596
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Newmark Agent');
|
|
597
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Newmark Agent');
|
|
598
598
|
}
|
|
599
599
|
function migrateLegacyRuntimeRoot(root) {
|
|
600
600
|
const targetRoot = path.resolve(root);
|
|
@@ -677,6 +677,15 @@ function resolveRoot(args) {
|
|
|
677
677
|
return writableRuntimeRoot(explicitRoot);
|
|
678
678
|
return getRoot();
|
|
679
679
|
}
|
|
680
|
+
function resolveTuiWorkspacePath(args, root) {
|
|
681
|
+
const explicitWorkspace = pathArgValue(args, '--workspace');
|
|
682
|
+
if (explicitWorkspace)
|
|
683
|
+
return explicitWorkspace;
|
|
684
|
+
// An explicitly isolated runtime must not silently register the caller's
|
|
685
|
+
// cwd as an external workspace. Keep the opt-in --workspace escape hatch,
|
|
686
|
+
// while making the safe one-argument form fully self-contained.
|
|
687
|
+
return pathArgValue(args, '--root') ? root : process.cwd();
|
|
688
|
+
}
|
|
680
689
|
function startupLogPath() {
|
|
681
690
|
try {
|
|
682
691
|
const userData = userRuntimeRoot();
|
|
@@ -1026,24 +1035,54 @@ const args = userArgs();
|
|
|
1026
1035
|
const command = args.find(a => a === 'flow' || a === 'edit');
|
|
1027
1036
|
const isTuiArg = args.some(arg => arg.toLowerCase() === '--tui');
|
|
1028
1037
|
const hasCliCommand = args.some(a => cli_commands_1.CLI_COMMANDS.includes(a));
|
|
1038
|
+
const isFlowArg = command === 'flow';
|
|
1039
|
+
const isEditArg = command === 'edit';
|
|
1029
1040
|
const isHelpArg = !hasCliCommand && (args.some(arg => ['--help', '-h'].includes(arg.toLowerCase())) || args[0]?.toLowerCase() === 'help');
|
|
1030
|
-
const isVersionArg = !hasCliCommand &&
|
|
1041
|
+
const isVersionArg = !hasCliCommand && (0, cli_discovery_1.isVersionArgument)(args);
|
|
1042
|
+
const isReadOnlyValidation = hasCliCommand && args.includes('validate-models') && !args.includes('--persist');
|
|
1031
1043
|
const isViewerArg = args.some(arg => arg.toLowerCase() === '--newmark-viewer');
|
|
1032
1044
|
const isCliArg = args.includes('--cli');
|
|
1033
1045
|
const isServerArg = args.includes('--server');
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1046
|
+
const invalidArgument = (0, cli_discovery_1.invalidTopLevelArgument)(args);
|
|
1047
|
+
if (invalidArgument) {
|
|
1048
|
+
console.error(`Invalid Newmark argument: ${invalidArgument}`);
|
|
1049
|
+
process.exit(2);
|
|
1050
|
+
}
|
|
1051
|
+
// Electron's Chromium profile is a separate state boundary from Newmark's
|
|
1052
|
+
// business root. Bind both before any ready event so --root cannot leave
|
|
1053
|
+
// Preferences, DIPS, DevTools ports, cookies, or session storage in the real
|
|
1054
|
+
// default AppData directory. The dedicated subdirectories keep Chromium's
|
|
1055
|
+
// files separate from the durable Newmark config/workspace files.
|
|
1056
|
+
const runtimeRoot = resolveRoot(args);
|
|
1057
|
+
const electronUserDataRoot = path.join(runtimeRoot, 'Electron');
|
|
1058
|
+
const electronSessionDataRoot = path.join(electronUserDataRoot, 'session-data');
|
|
1059
|
+
try {
|
|
1060
|
+
fs.mkdirSync(electronSessionDataRoot, { recursive: true });
|
|
1061
|
+
electron_1.app.setPath('userData', electronUserDataRoot);
|
|
1062
|
+
electron_1.app.setPath('sessionData', electronSessionDataRoot);
|
|
1063
|
+
}
|
|
1064
|
+
catch (error) {
|
|
1065
|
+
console.error(`Unable to isolate Electron user-data directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1036
1068
|
// Help/version are terminating discovery commands. They must be handled
|
|
1037
1069
|
// before Electron's GUI/TUI/server branches can initialize runtime state or
|
|
1038
1070
|
// spawn a window, so a product-new tester can use them safely in any surface.
|
|
1039
1071
|
if (isHelpArg) {
|
|
1040
|
-
console.log((0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
|
|
1072
|
+
console.log(isFlowArg ? (0, cli_help_1.newmarkFlowHelpText)() : isEditArg ? (0, cli_help_1.newmarkEditHelpText)() : (0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
|
|
1041
1073
|
process.exit(0);
|
|
1042
1074
|
}
|
|
1043
1075
|
if (isVersionArg) {
|
|
1044
1076
|
console.log((0, installUpdate_1.currentAppVersion)());
|
|
1045
1077
|
process.exit(0);
|
|
1046
1078
|
}
|
|
1079
|
+
const unknownCommand = electron_1.app.isPackaged && !hasCliCommand && !isTuiArg && !isCliArg && !isServerArg && !isViewerArg
|
|
1080
|
+
? (0, cli_discovery_1.unknownTopLevelCommand)(args)
|
|
1081
|
+
: undefined;
|
|
1082
|
+
if (unknownCommand) {
|
|
1083
|
+
console.error(`Unknown Newmark command or argument: ${unknownCommand}. Run --help to see the supported entrypoints.`);
|
|
1084
|
+
process.exit(2);
|
|
1085
|
+
}
|
|
1047
1086
|
function viewerEscape(value) {
|
|
1048
1087
|
return String(value || '').replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character] || character);
|
|
1049
1088
|
}
|
|
@@ -1118,7 +1157,9 @@ if (isViewerArg) {
|
|
|
1118
1157
|
}).catch(error => { console.error(`Unable to open Newmark viewer: ${error instanceof Error ? error.message : String(error)}`); electron_1.app.quit(); });
|
|
1119
1158
|
}
|
|
1120
1159
|
else if (isTuiArg) {
|
|
1121
|
-
const isConsoleLauncher = electron_1.app.isPackaged && path.basename(process.execPath).toLowerCase() === 'newmark.exe'
|
|
1160
|
+
const isConsoleLauncher = electron_1.app.isPackaged && (path.basename(process.execPath).toLowerCase() === 'newmark.exe'
|
|
1161
|
+
|| path.basename(process.execPath).toLowerCase() === 'newmark console runtime.exe'
|
|
1162
|
+
|| process.env.NEWMARK_CONSOLE_WRAPPER === '1');
|
|
1122
1163
|
if (isConsoleLauncher && process.env.NEWMARK_TUI_SIDECAR !== '1') {
|
|
1123
1164
|
const tuiProcess = (0, child_process_1.spawnSync)(process.execPath, [path.join(__dirname, 'launcher.js'), ...args], {
|
|
1124
1165
|
cwd: process.cwd(),
|
|
@@ -1126,6 +1167,11 @@ else if (isTuiArg) {
|
|
|
1126
1167
|
...process.env,
|
|
1127
1168
|
ELECTRON_RUN_AS_NODE: '1',
|
|
1128
1169
|
NEWMARK_TUI_SIDECAR: '1',
|
|
1170
|
+
// The GUI Electron host does not expose the inherited ConPTY handles
|
|
1171
|
+
// as Node TTY streams. The console wrapper has already established
|
|
1172
|
+
// that this is the terminal entrypoint, so preserve the terminal
|
|
1173
|
+
// contract for the sidecar without weakening ordinary GUI launches.
|
|
1174
|
+
NEWMARK_FORCE_TTY: isConsoleLauncher ? '1' : process.env.NEWMARK_FORCE_TTY,
|
|
1129
1175
|
},
|
|
1130
1176
|
stdio: 'inherit',
|
|
1131
1177
|
windowsHide: false,
|
|
@@ -1146,12 +1192,12 @@ else if (isTuiArg) {
|
|
|
1146
1192
|
const root = resolveRoot(args);
|
|
1147
1193
|
firstRunInit(root);
|
|
1148
1194
|
const { start } = require('./tui/src/app');
|
|
1149
|
-
start({ root, workspacePath:
|
|
1195
|
+
start({ root, workspacePath: resolveTuiWorkspacePath(args, root), desktopDist: __dirname });
|
|
1150
1196
|
}
|
|
1151
1197
|
else if (hasCliCommand) {
|
|
1152
1198
|
(async () => {
|
|
1153
1199
|
const root = resolveRoot(args);
|
|
1154
|
-
firstRunInit(root);
|
|
1200
|
+
firstRunInit(root, { readOnly: isReadOnlyValidation });
|
|
1155
1201
|
const handled = await (0, cli_commands_1.runCliCommand)(root, args);
|
|
1156
1202
|
const code = typeof process.exitCode === 'number' ? process.exitCode : 0;
|
|
1157
1203
|
exitCli(handled ? code : 1);
|
|
@@ -1312,6 +1358,51 @@ else {
|
|
|
1312
1358
|
});
|
|
1313
1359
|
electron_1.app.whenReady().then(async () => {
|
|
1314
1360
|
let root = resolveRoot(args);
|
|
1361
|
+
let workspaceRegistryWatcher = null;
|
|
1362
|
+
let workspaceRegistryWatchTimer = null;
|
|
1363
|
+
const workspaceRegistryFiles = new Set(['Local.json', 'External.json', 'State.json']);
|
|
1364
|
+
const broadcastWorkspaceChanged = (files) => {
|
|
1365
|
+
for (const win of electron_1.BrowserWindow.getAllWindows()) {
|
|
1366
|
+
if (win.isDestroyed())
|
|
1367
|
+
continue;
|
|
1368
|
+
win.webContents.send('workspace:changed', { files, root });
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const refreshWorkspaceRegistryFromDisk = (files) => {
|
|
1372
|
+
if (!agent)
|
|
1373
|
+
return;
|
|
1374
|
+
agent.refreshWorkspaceRegistryFromStorage();
|
|
1375
|
+
const current = agent.workspace.current;
|
|
1376
|
+
if (current)
|
|
1377
|
+
workspaceSelectionCoordinator?.setCurrent(current.id || current.path || current.name);
|
|
1378
|
+
broadcastWorkspaceChanged(files);
|
|
1379
|
+
};
|
|
1380
|
+
const ensureWorkspaceRegistryWatcher = () => {
|
|
1381
|
+
if (workspaceRegistryWatcher)
|
|
1382
|
+
return;
|
|
1383
|
+
const workDir = path.join(root, 'Work');
|
|
1384
|
+
try {
|
|
1385
|
+
workspaceRegistryWatcher = fs.watch(workDir, { persistent: false }, (_eventType, filename) => {
|
|
1386
|
+
const changed = String(filename || '');
|
|
1387
|
+
if (changed && !workspaceRegistryFiles.has(path.basename(changed)))
|
|
1388
|
+
return;
|
|
1389
|
+
if (workspaceRegistryWatchTimer)
|
|
1390
|
+
clearTimeout(workspaceRegistryWatchTimer);
|
|
1391
|
+
workspaceRegistryWatchTimer = setTimeout(() => {
|
|
1392
|
+
workspaceRegistryWatchTimer = null;
|
|
1393
|
+
try {
|
|
1394
|
+
refreshWorkspaceRegistryFromDisk(changed ? [path.basename(changed)] : ['Work']);
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
logStartupFailure('workspace-registry-refresh', error);
|
|
1398
|
+
}
|
|
1399
|
+
}, 90);
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
logStartupFailure('workspace-registry-watch', error);
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1315
1406
|
const fileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
|
|
1316
1407
|
const pdfPreviewServer = new pdfPreviewServer_1.PdfPreviewServer((token, ownerId) => fileRouter.resolvePdfCapability(token, ownerId));
|
|
1317
1408
|
await pdfPreviewServer.start();
|
|
@@ -1718,6 +1809,7 @@ else {
|
|
|
1718
1809
|
agent = new agent_1.Agent(root);
|
|
1719
1810
|
mcpManager = new mcpManager_1.McpManager(root);
|
|
1720
1811
|
activeAgentBackendMode = process.platform === 'win32' && agent.config.getBool('agent', 'run_in_wsl') ? 'wsl' : 'windows';
|
|
1812
|
+
ensureWorkspaceRegistryWatcher();
|
|
1721
1813
|
restoreStoredFlowSuspension();
|
|
1722
1814
|
recordStartup('agent-ready');
|
|
1723
1815
|
}
|
|
@@ -2207,8 +2299,17 @@ else {
|
|
|
2207
2299
|
(0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
|
|
2208
2300
|
};
|
|
2209
2301
|
electron_1.app.on('will-quit', event => {
|
|
2210
|
-
|
|
2302
|
+
// Window-close and tray-exit both enter this path. The runtime pools
|
|
2303
|
+
// must get a bounded graceful-shutdown window regardless of which
|
|
2304
|
+
// surface initiated the close; otherwise a stuck child can keep the
|
|
2305
|
+
// Electron parent and its renderer tree alive indefinitely.
|
|
2306
|
+
if (_forceQuit || !appExitCleanupComplete)
|
|
2211
2307
|
armForcedExitDeadline('will-quit');
|
|
2308
|
+
if (workspaceRegistryWatchTimer)
|
|
2309
|
+
clearTimeout(workspaceRegistryWatchTimer);
|
|
2310
|
+
workspaceRegistryWatchTimer = null;
|
|
2311
|
+
workspaceRegistryWatcher?.close();
|
|
2312
|
+
workspaceRegistryWatcher = null;
|
|
2212
2313
|
startupDeferredTasks?.cancel();
|
|
2213
2314
|
agent?.flushWorkspaceConversationState();
|
|
2214
2315
|
conversationKernel?.flushPersistence();
|
|
@@ -2539,6 +2640,13 @@ else {
|
|
|
2539
2640
|
else
|
|
2540
2641
|
await electronUtilityRuntimePool?.stopTarget(target);
|
|
2541
2642
|
};
|
|
2643
|
+
const forceStopTargetRuntime = async (target) => {
|
|
2644
|
+
if (wslBackendEnabled())
|
|
2645
|
+
await wslAgentRuntimePool?.forceStopTarget(target);
|
|
2646
|
+
else
|
|
2647
|
+
await electronUtilityRuntimePool?.forceStopTarget(target);
|
|
2648
|
+
};
|
|
2649
|
+
const archiveInFlight = new Map();
|
|
2542
2650
|
const isolatedConversationAgent = (target) => {
|
|
2543
2651
|
const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
|
|
2544
2652
|
const isolated = new agent_1.Agent(root, { agentOnly: true });
|
|
@@ -3732,32 +3840,46 @@ else {
|
|
|
3732
3840
|
return { ok: false, error: 'Agent not initialized' };
|
|
3733
3841
|
const target = conversationRuntimeTarget(targetInput || agent.activeConversationId || 'default');
|
|
3734
3842
|
const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
|
|
3735
|
-
|
|
3736
|
-
|
|
3843
|
+
const existing = archiveInFlight.get(normalized.runtimeKey);
|
|
3844
|
+
if (existing)
|
|
3845
|
+
return existing;
|
|
3846
|
+
const operation = (async () => {
|
|
3847
|
+
mutatingRuntimeKeys.add(normalized.runtimeKey);
|
|
3848
|
+
try {
|
|
3849
|
+
// Archive is a destructive lifecycle command. It intentionally
|
|
3850
|
+
// bypasses the normal mutation/active-prompt guard and hard-stops
|
|
3851
|
+
// any resident runtime before touching conversation persistence.
|
|
3852
|
+
await forceStopTargetRuntime(normalized);
|
|
3853
|
+
const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
|
|
3854
|
+
const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
|
|
3855
|
+
const ownsTargetWorkspace = !!normalized.workspace
|
|
3856
|
+
&& !!agent.workspace.current
|
|
3857
|
+
&& currentWorkspacePath === targetWorkspacePath;
|
|
3858
|
+
// The host Agent owns the current workspace persistence cache. An
|
|
3859
|
+
// isolated owner is used for another workspace. The archive writer
|
|
3860
|
+
// starts payload I/O in parallel and finalizes deletion against the
|
|
3861
|
+
// latest locked state snapshot, so rapid clicks do not serialize on
|
|
3862
|
+
// large Markdown bodies or lose a sibling deletion.
|
|
3863
|
+
const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
|
|
3864
|
+
const archived = await archiveOwner.archiveConversationAsync(normalized.conversationId);
|
|
3865
|
+
if (!archived)
|
|
3866
|
+
return { ok: false, error: 'Conversation archive could not be written.' };
|
|
3867
|
+
return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
|
|
3868
|
+
}
|
|
3869
|
+
catch (error) {
|
|
3870
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
3871
|
+
}
|
|
3872
|
+
finally {
|
|
3873
|
+
mutatingRuntimeKeys.delete(normalized.runtimeKey);
|
|
3874
|
+
}
|
|
3875
|
+
})();
|
|
3876
|
+
archiveInFlight.set(normalized.runtimeKey, operation);
|
|
3737
3877
|
try {
|
|
3738
|
-
|
|
3739
|
-
if (peek.running || peek.stopping)
|
|
3740
|
-
return { ok: false, error: 'Cannot archive a conversation while its runtime is running or stopping.' };
|
|
3741
|
-
if (peek.resident)
|
|
3742
|
-
await stopTargetRuntime(normalized);
|
|
3743
|
-
const currentWorkspacePath = path.resolve(agent.workspace.current?.path || '');
|
|
3744
|
-
const targetWorkspacePath = path.resolve(normalized.workspace?.path || '');
|
|
3745
|
-
const ownsTargetWorkspace = !!normalized.workspace
|
|
3746
|
-
&& !!agent.workspace.current
|
|
3747
|
-
&& currentWorkspacePath === targetWorkspacePath;
|
|
3748
|
-
// The host Agent owns the current workspace persistence cache. Archiving
|
|
3749
|
-
// through it prevents a delayed host flush from resurrecting the target.
|
|
3750
|
-
const archiveOwner = ownsTargetWorkspace ? agent : isolatedConversationAgent(normalized);
|
|
3751
|
-
const archived = archiveOwner.archiveConversation(normalized.conversationId);
|
|
3752
|
-
if (!archived)
|
|
3753
|
-
return { ok: false, error: 'Conversation archive could not be written.' };
|
|
3754
|
-
return { ok: true, fileName: archived, conversationId: normalized.conversationId, workspaceId: normalized.workspaceId };
|
|
3755
|
-
}
|
|
3756
|
-
catch (error) {
|
|
3757
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
3878
|
+
return await operation;
|
|
3758
3879
|
}
|
|
3759
3880
|
finally {
|
|
3760
|
-
|
|
3881
|
+
if (archiveInFlight.get(normalized.runtimeKey) === operation)
|
|
3882
|
+
archiveInFlight.delete(normalized.runtimeKey);
|
|
3761
3883
|
}
|
|
3762
3884
|
});
|
|
3763
3885
|
electron_1.ipcMain.handle('agent:listArchives', async (_event, scope) => {
|
package/dist/preload.js
CHANGED
|
@@ -173,6 +173,12 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
173
173
|
onAgentWorkEvent: (callback) => {
|
|
174
174
|
ipcRenderer.on('agent:workEvent', callback);
|
|
175
175
|
},
|
|
176
|
+
onWorkspaceChanged: (callback) => {
|
|
177
|
+
ipcRenderer.on('workspace:changed', (_event, payload) => callback(payload));
|
|
178
|
+
},
|
|
179
|
+
removeWorkspaceChangedListener: () => {
|
|
180
|
+
ipcRenderer.removeAllListeners('workspace:changed');
|
|
181
|
+
},
|
|
176
182
|
removeAgentWorkEventListener: () => {
|
|
177
183
|
ipcRenderer.removeAllListeners('agent:workEvent');
|
|
178
184
|
},
|
|
@@ -96,11 +96,7 @@ class ChatCompletionsAdapter {
|
|
|
96
96
|
let emittedTool = false;
|
|
97
97
|
try {
|
|
98
98
|
while (true) {
|
|
99
|
-
|
|
100
|
-
throw (0, provider_events_1.providerAbortError)(signal);
|
|
101
|
-
const readPromise = reader.read();
|
|
102
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Stream read timeout')), 30000));
|
|
103
|
-
const { done, value } = await Promise.race([readPromise, timeoutPromise]);
|
|
99
|
+
const { done, value } = await (0, provider_events_1.readProviderStreamChunk)(reader, signal);
|
|
104
100
|
if (done)
|
|
105
101
|
break;
|
|
106
102
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -12,6 +12,13 @@ export declare function defaultProviderTransport(request: SerializedProviderRequ
|
|
|
12
12
|
* (`name === 'AbortError'`, preserves the abort reason when present).
|
|
13
13
|
*/
|
|
14
14
|
export declare function providerAbortError(signal?: AbortSignal): Error;
|
|
15
|
+
export declare function providerStreamTimeoutError(timeoutMs: number): Error;
|
|
16
|
+
/**
|
|
17
|
+
* Read one SSE chunk with both user cancellation and an inactivity deadline.
|
|
18
|
+
* Cancelling the reader is important: rejecting the race alone leaves the
|
|
19
|
+
* provider socket alive and lets later requests accumulate behind it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readProviderStreamChunk(reader: ReadableStreamDefaultReader<Uint8Array>, signal: AbortSignal, timeoutMs?: number): Promise<ReadableStreamReadResult<Uint8Array>>;
|
|
15
22
|
export declare function parseProviderSse(raw: string): Array<{
|
|
16
23
|
event?: string;
|
|
17
24
|
data: string;
|