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.
Files changed (39) hide show
  1. package/config.example.json +6 -0
  2. package/dist/cli-commands.d.ts +7 -0
  3. package/dist/cli-commands.js +206 -15
  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/conversation-utility-host.bundle.cjs +357 -84
  9. package/dist/core/agent.d.ts +18 -3
  10. package/dist/core/agent.js +214 -30
  11. package/dist/core/agentKernelRunner.js +53 -8
  12. package/dist/core/config.d.ts +7 -2
  13. package/dist/core/config.js +24 -6
  14. package/dist/core/conversationKernel.js +1 -1
  15. package/dist/core/electronUtilityRuntimePool.d.ts +11 -0
  16. package/dist/core/electronUtilityRuntimePool.js +62 -0
  17. package/dist/core/flow-runner.js +1 -1
  18. package/dist/core/modelValidationStore.d.ts +4 -1
  19. package/dist/core/modelValidationStore.js +7 -1
  20. package/dist/core/workspace.d.ts +6 -0
  21. package/dist/core/workspace.js +14 -0
  22. package/dist/core/wslAgentRuntimePool.d.ts +4 -0
  23. package/dist/core/wslAgentRuntimePool.js +56 -0
  24. package/dist/launcher.js +40 -11
  25. package/dist/llm/provider.d.ts +8 -5
  26. package/dist/llm/provider.js +85 -33
  27. package/dist/main.js +167 -45
  28. package/dist/preload.js +6 -0
  29. package/dist/providers/chat-completions.adapter.js +1 -5
  30. package/dist/providers/provider-events.d.ts +7 -0
  31. package/dist/providers/provider-events.js +44 -0
  32. package/dist/providers/responses.adapter.js +1 -3
  33. package/dist/tui/src/adapters/core-runtime-adapter.js +40 -3
  34. package/dist/tui/src/app.js +47 -13
  35. package/dist/tui/src/render.js +23 -7
  36. package/dist/tui/src/state.js +61 -9
  37. package/dist/ui/index.html +250 -84
  38. package/dist/wsl-agent-host.bundle.cjs +357 -84
  39. package/package.json +14 -5
@@ -64,8 +64,10 @@ class ConfigManager {
64
64
  rootPath;
65
65
  config;
66
66
  workspaceOverrides;
67
- constructor(rootPath) {
67
+ readOnly;
68
+ constructor(rootPath, options = {}) {
68
69
  this.rootPath = rootPath;
70
+ this.readOnly = options.readOnly === true;
69
71
  this.workspaceOverrides = new Map();
70
72
  this.config = this.load();
71
73
  }
@@ -80,6 +82,8 @@ class ConfigManager {
80
82
  const raw = JSON.parse(readJsonText(cp));
81
83
  const normalized = normalizeConfigShape(raw, true);
82
84
  if (isCorruptConfig(raw, normalized)) {
85
+ if (this.readOnly)
86
+ return defaultConfig();
83
87
  this.backupConfig(cp, 'invalid-shape');
84
88
  return this.writeRecoveredConfig(cp);
85
89
  }
@@ -87,7 +91,8 @@ class ConfigManager {
87
91
  // Provider ids are routing identities, so legacy/malformed catalogs must
88
92
  // not wait for an unrelated settings save before becoming collision-safe.
89
93
  try {
90
- fs.writeFileSync(cp, JSON.stringify(normalized, null, 2), 'utf-8');
94
+ if (!this.readOnly)
95
+ fs.writeFileSync(cp, JSON.stringify(normalized, null, 2), 'utf-8');
91
96
  }
92
97
  catch {
93
98
  // A read-only root may still be used for this process. The normalized
@@ -97,6 +102,8 @@ class ConfigManager {
97
102
  return normalized;
98
103
  }
99
104
  catch {
105
+ if (this.readOnly)
106
+ return defaultConfig();
100
107
  this.backupConfig(cp, 'invalid-json');
101
108
  return this.writeRecoveredConfig(cp);
102
109
  }
@@ -146,10 +153,14 @@ class ConfigManager {
146
153
  this.config[section][key] = { value: normalizedValue };
147
154
  }
148
155
  save() {
156
+ if (this.readOnly)
157
+ return;
149
158
  const j = JSON.stringify(this.config, null, 2);
150
159
  fs.writeFileSync(path.join(this.rootPath, 'config.json'), j, 'utf-8');
151
160
  }
152
161
  saveTo(targetPath) {
162
+ if (this.readOnly)
163
+ return;
153
164
  const j = JSON.stringify(this.config, null, 2);
154
165
  fs.writeFileSync(targetPath, j, 'utf-8');
155
166
  }
@@ -393,12 +404,16 @@ class ConfigManager {
393
404
  return providers;
394
405
  }
395
406
  writeRecoveredConfig(configPath) {
407
+ if (this.readOnly)
408
+ return defaultConfig();
396
409
  const config = loadExampleConfig();
397
410
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
398
411
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
399
412
  return config;
400
413
  }
401
414
  backupConfig(configPath, reason) {
415
+ if (this.readOnly)
416
+ return;
402
417
  try {
403
418
  if (!fs.existsSync(configPath))
404
419
  return;
@@ -412,13 +427,13 @@ class ConfigManager {
412
427
  }
413
428
  }
414
429
  exports.ConfigManager = ConfigManager;
415
- function ensureRootConfig(rootPath) {
430
+ function ensureRootConfig(rootPath, options = {}) {
416
431
  const configPath = path.join(rootPath, 'config.json');
417
432
  if (fs.existsSync(configPath)) {
418
- new ConfigManager(rootPath);
433
+ new ConfigManager(rootPath, options);
419
434
  return;
420
435
  }
421
- new ConfigManager(rootPath).save();
436
+ new ConfigManager(rootPath, options).save();
422
437
  }
423
438
  function normalizeConfigShape(raw, withDefaults) {
424
439
  const base = withDefaults ? defaultConfig() : {};
@@ -827,7 +842,10 @@ function defaultConfig() {
827
842
  general: {
828
843
  tone: { _description: "Conversation style", _type: "choice", _values: ["strict_simple", "casual_friendly"], value: "strict_simple" },
829
844
  language: { _description: "Default language", _type: "choice", _values: ["en", "zh", "auto"], value: "auto" },
830
- close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "minimize" },
845
+ // A first-run desktop window must have a deterministic close/exit
846
+ // contract. Users who explicitly choose minimize-to-tray keep that
847
+ // choice, but a fresh install must not hide the process on OS close.
848
+ close_behavior: { _description: "Close behavior", _type: "choice", _values: ["minimize", "exit"], value: "exit" },
831
849
  default_input: { _description: "Default input mode", _type: "choice", _values: ["guide", "next"], value: "guide" },
832
850
  auto_archive_on_close: { _description: "Auto archive on close", _type: "boolean", value: true },
833
851
  },
@@ -499,7 +499,7 @@ class ConversationKernel {
499
499
  stopped = true;
500
500
  }
501
501
  else {
502
- runtime.runner.finishConversationWorkRun(runId, 'error');
502
+ runtime.runner.finishConversationWorkRun(runId, 'error', undefined, error instanceof Error ? error.message : String(error));
503
503
  throw error;
504
504
  }
505
505
  }
@@ -55,6 +55,7 @@ export declare class ElectronUtilityRuntimePool {
55
55
  private restarting;
56
56
  private quarantined;
57
57
  private disposing;
58
+ private forceStopPromises;
58
59
  private capacityTail;
59
60
  private accessSequence;
60
61
  constructor(root: string, hostScript: string, createClient?: ElectronTargetRuntimeClientFactory, options?: ElectronUtilityRuntimePoolOptions);
@@ -86,6 +87,16 @@ export declare class ElectronUtilityRuntimePool {
86
87
  hasActiveWorkspace(target: ConversationRuntimeTarget): Promise<boolean>;
87
88
  stopWorkspace(target: ConversationRuntimeTarget): Promise<void>;
88
89
  stopTarget(target: ConversationRuntimeTarget): Promise<void>;
90
+ /**
91
+ * Immediately terminate a target runtime for destructive lifecycle actions.
92
+ * Archive is deliberately stronger than the user-facing two-click Stop
93
+ * contract: the target may be running, already stopping, or holding an
94
+ * active prompt lease. Give the kernel only the short checkpoint window
95
+ * owned by forceTerminateEntry, then hard-stop and evict the client so no
96
+ * delayed runtime event can keep the archived target resident.
97
+ */
98
+ forceStopTarget(target: ConversationRuntimeTarget): Promise<void>;
99
+ private forceStopTargetInternal;
89
100
  stopAll(): Promise<void>;
90
101
  private throwStopFailures;
91
102
  private stopEntry;
@@ -16,6 +16,7 @@ class ElectronUtilityRuntimePool {
16
16
  restarting = new Set();
17
17
  quarantined = new Map();
18
18
  disposing = new Set();
19
+ forceStopPromises = new Map();
19
20
  capacityTail = Promise.resolve();
20
21
  accessSequence = 0;
21
22
  constructor(root, hostScript, createClient = target => new electronUtilityAgentClient_1.ElectronUtilityAgentClient(root, hostScript, target), options = {}) {
@@ -342,6 +343,67 @@ class ElectronUtilityRuntimePool {
342
343
  if (entry)
343
344
  await this.stopEntry(entry);
344
345
  }
346
+ /**
347
+ * Immediately terminate a target runtime for destructive lifecycle actions.
348
+ * Archive is deliberately stronger than the user-facing two-click Stop
349
+ * contract: the target may be running, already stopping, or holding an
350
+ * active prompt lease. Give the kernel only the short checkpoint window
351
+ * owned by forceTerminateEntry, then hard-stop and evict the client so no
352
+ * delayed runtime event can keep the archived target resident.
353
+ */
354
+ async forceStopTarget(target) {
355
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
356
+ const existing = this.forceStopPromises.get(normalized.runtimeKey);
357
+ if (existing)
358
+ return existing;
359
+ const operation = this.forceStopTargetInternal(normalized);
360
+ this.forceStopPromises.set(normalized.runtimeKey, operation);
361
+ try {
362
+ await operation;
363
+ }
364
+ finally {
365
+ if (this.forceStopPromises.get(normalized.runtimeKey) === operation) {
366
+ this.forceStopPromises.delete(normalized.runtimeKey);
367
+ }
368
+ }
369
+ }
370
+ async forceStopTargetInternal(target) {
371
+ let entry;
372
+ await this.serializeCapacity(async () => {
373
+ entry = this.entries.get(target.runtimeKey);
374
+ if (entry)
375
+ this.disposing.add(target.runtimeKey);
376
+ });
377
+ if (!entry)
378
+ return;
379
+ const intent = entry.stopIntent || {
380
+ runId: entry.lastRunId,
381
+ generation: entry.lastGeneration,
382
+ checkpointed: false,
383
+ forcePromise: null,
384
+ };
385
+ if (!entry.stopIntent)
386
+ entry.stopIntent = intent;
387
+ try {
388
+ await this.forceTerminateEntry(entry, intent);
389
+ // forceStop is expected to disconnect the utility child. If a client
390
+ // reports a stale connected flag, retry the hard boundary once before
391
+ // declaring archive unsafe.
392
+ if (entry.client.status().connected)
393
+ await entry.client.forceStop();
394
+ if (entry.client.status().connected) {
395
+ throw new Error(`Electron utility runtime ${target.runtimeKey} remained connected after force stop`);
396
+ }
397
+ if (this.entries.get(target.runtimeKey) === entry) {
398
+ this.entries.delete(target.runtimeKey);
399
+ entry.unsubscribe();
400
+ entry.client.setHostToolHandler(null);
401
+ }
402
+ }
403
+ finally {
404
+ this.disposing.delete(target.runtimeKey);
405
+ }
406
+ }
345
407
  async stopAll() {
346
408
  const targets = Array.from(this.entries.values(), entry => entry.target);
347
409
  const results = await Promise.allSettled(targets.map(target => this.stopTarget(target)));
@@ -126,7 +126,7 @@ async function runFlowBuild(agent, prompt, options) {
126
126
  if (!options.signal?.aborted && typeof agent.emitWorkEvent === 'function') {
127
127
  agent.emitWorkEvent({ type: 'error', content: reportedError.message, runId });
128
128
  }
129
- agent.finishConversationWorkRun(runId, options.signal?.aborted ? 'interrupted' : 'error');
129
+ agent.finishConversationWorkRun(runId, options.signal?.aborted ? 'interrupted' : 'error', undefined, options.signal?.aborted ? '' : reportedError.message);
130
130
  agent.flushWorkspaceConversationState();
131
131
  }
132
132
  throw reportedError;
@@ -2,8 +2,11 @@ import { ModelValidationCache, ModelValidationRecord } from './modelValidation';
2
2
  /** Durable validation evidence. The file contains probe metadata only, never prompts, tool arguments or credentials. */
3
3
  export declare class FileModelValidationCache implements ModelValidationCache {
4
4
  private readonly filePath;
5
+ private readonly readOnly;
5
6
  private readonly records;
6
- constructor(rootPath: string);
7
+ constructor(rootPath: string, options?: {
8
+ readOnly?: boolean;
9
+ });
7
10
  get(modelKey: string): ModelValidationRecord | undefined;
8
11
  set(record: ModelValidationRecord): void;
9
12
  delete(modelKey: string): void;
@@ -42,9 +42,11 @@ function key(model) {
42
42
  /** Durable validation evidence. The file contains probe metadata only, never prompts, tool arguments or credentials. */
43
43
  class FileModelValidationCache {
44
44
  filePath;
45
+ readOnly;
45
46
  records = new Map();
46
- constructor(rootPath) {
47
+ constructor(rootPath, options = {}) {
47
48
  this.filePath = path.join(rootPath, 'model-validation', 'records.json');
49
+ this.readOnly = options.readOnly === true;
48
50
  this.load();
49
51
  }
50
52
  get(modelKey) {
@@ -53,11 +55,15 @@ class FileModelValidationCache {
53
55
  }
54
56
  set(record) {
55
57
  this.records.set(record.modelKey || key(record.model), JSON.parse(JSON.stringify(record)));
58
+ if (this.readOnly)
59
+ return;
56
60
  this.save();
57
61
  }
58
62
  delete(modelKey) {
59
63
  if (!this.records.delete(modelKey))
60
64
  return;
65
+ if (this.readOnly)
66
+ return;
61
67
  this.save();
62
68
  }
63
69
  load() {
@@ -52,6 +52,12 @@ export declare class WorkspaceManager {
52
52
  private saveState;
53
53
  private findWorkspace;
54
54
  private restoreCurrent;
55
+ /**
56
+ * Re-read the registry and persisted current-workspace pointer after another
57
+ * Newmark entrypoint updates Work/*.json. This intentionally does not create
58
+ * a workspace: a refresh must reflect the shared on-disk state exactly.
59
+ */
60
+ reloadFromStorage(): WorkspaceInfo | null;
55
61
  private saveInternal;
56
62
  private saveExternal;
57
63
  private sleepSync;
@@ -358,6 +358,20 @@ class WorkspaceManager {
358
358
  this.saveState();
359
359
  }
360
360
  }
361
+ /**
362
+ * Re-read the registry and persisted current-workspace pointer after another
363
+ * Newmark entrypoint updates Work/*.json. This intentionally does not create
364
+ * a workspace: a refresh must reflect the shared on-disk state exactly.
365
+ */
366
+ reloadFromStorage() {
367
+ if (this.detached)
368
+ return this.current;
369
+ this.scan();
370
+ this.validate();
371
+ this.current = null;
372
+ this.restoreCurrent();
373
+ return this.current;
374
+ }
361
375
  saveInternal() {
362
376
  if (this.detached)
363
377
  return;
@@ -56,6 +56,7 @@ export declare class WslAgentRuntimePool {
56
56
  private hostToolHandler;
57
57
  private restarting;
58
58
  private disposing;
59
+ private forceStopPromises;
59
60
  private capacityTail;
60
61
  private accessSequence;
61
62
  constructor(distro: string, windowsRoot: string, windowsHostScript: string, createClient?: WslTargetRuntimeClientFactory, options?: WslAgentRuntimePoolOptions);
@@ -90,6 +91,9 @@ export declare class WslAgentRuntimePool {
90
91
  hasActiveWorkspace(target: ConversationRuntimeTarget): Promise<boolean>;
91
92
  stopWorkspace(target: ConversationRuntimeTarget): Promise<void>;
92
93
  stopTarget(target: ConversationRuntimeTarget): Promise<void>;
94
+ /** Hard-stop a target for destructive lifecycle actions such as archive. */
95
+ forceStopTarget(target: ConversationRuntimeTarget): Promise<void>;
96
+ private forceStopTargetInternal;
93
97
  stopAll(): Promise<void>;
94
98
  private throwStopFailures;
95
99
  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 = {}) {
@@ -356,6 +357,61 @@ class WslAgentRuntimePool {
356
357
  if (entry)
357
358
  await this.stopEntry(entry);
358
359
  }
360
+ /** Hard-stop a target for destructive lifecycle actions such as archive. */
361
+ async forceStopTarget(target) {
362
+ const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
363
+ const existing = this.forceStopPromises.get(normalized.runtimeKey);
364
+ if (existing)
365
+ return existing;
366
+ const operation = this.forceStopTargetInternal(normalized);
367
+ this.forceStopPromises.set(normalized.runtimeKey, operation);
368
+ try {
369
+ await operation;
370
+ }
371
+ finally {
372
+ if (this.forceStopPromises.get(normalized.runtimeKey) === operation) {
373
+ this.forceStopPromises.delete(normalized.runtimeKey);
374
+ }
375
+ }
376
+ }
377
+ async forceStopTargetInternal(target) {
378
+ let entry;
379
+ await this.serializeCapacity(async () => {
380
+ entry = this.entries.get(target.runtimeKey);
381
+ if (entry)
382
+ this.disposing.add(target.runtimeKey);
383
+ });
384
+ if (!entry)
385
+ return;
386
+ const intent = entry.stopIntent || {
387
+ runId: entry.lastRunId,
388
+ generation: entry.lastGeneration,
389
+ checkpointed: false,
390
+ forcePromise: null,
391
+ };
392
+ if (!entry.stopIntent)
393
+ entry.stopIntent = intent;
394
+ try {
395
+ await this.forceTerminateEntry(entry, intent);
396
+ if (entry.client.status().connected) {
397
+ if (entry.client.forceStopRuntimeGroup)
398
+ await entry.client.forceStopRuntimeGroup();
399
+ else
400
+ await entry.client.forceRestartRuntimeGroup();
401
+ }
402
+ if (entry.client.status().connected) {
403
+ throw new Error(`WSL runtime ${target.runtimeKey} remained connected after force stop`);
404
+ }
405
+ if (this.entries.get(target.runtimeKey) === entry) {
406
+ this.entries.delete(target.runtimeKey);
407
+ entry.unsubscribe();
408
+ entry.client.setHostToolHandler(null);
409
+ }
410
+ }
411
+ finally {
412
+ this.disposing.delete(target.runtimeKey);
413
+ }
414
+ }
359
415
  async stopAll() {
360
416
  const targets = Array.from(this.entries.values(), entry => entry.target);
361
417
  const results = await Promise.allSettled(targets.map(target => this.stopTarget(target)));
package/dist/launcher.js CHANGED
@@ -43,19 +43,27 @@ 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 isEdit = args[0] === 'edit';
53
+ const editFile = isEdit ? args[1] : '';
54
+ const isFlow = args[0] === 'flow';
50
55
  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()));
56
+ const isVersionArg = !hasCliCommand && (0, cli_discovery_1.isVersionArgument)(args);
57
+ const isReadOnlyValidation = hasCliCommand && args.includes('validate-models') && !args.includes('--persist');
52
58
  const isTui = args.some(arg => arg.toLowerCase() === '--tui');
53
59
  const isGui = args.some(arg => arg.toLowerCase() === '--gui');
54
60
  const isCli = args.includes('--cli');
55
61
  const isServer = args.includes('--server');
56
- const isEdit = args[0] === 'edit';
57
- const editFile = isEdit ? args[1] : '';
58
- const isFlow = args[0] === 'flow';
62
+ const invalidArgument = (0, cli_discovery_1.invalidTopLevelArgument)(args);
63
+ if (invalidArgument) {
64
+ console.error(`Invalid Newmark argument: ${invalidArgument}`);
65
+ process.exit(2);
66
+ }
59
67
  function pathArgValue(values, key) {
60
68
  const prefix = `${key}=`;
61
69
  const inlineIdx = values.findIndex(a => a.startsWith(prefix));
@@ -89,6 +97,15 @@ function pathArgValue(values, key) {
89
97
  }
90
98
  return best || parts.join(' ') || undefined;
91
99
  }
100
+ function resolveTuiWorkspacePath(values, root) {
101
+ const explicitWorkspace = pathArgValue(values, '--workspace');
102
+ if (explicitWorkspace)
103
+ return explicitWorkspace;
104
+ // An explicitly isolated runtime must not silently register the caller's
105
+ // cwd as an external workspace. Keep the opt-in --workspace escape hatch,
106
+ // while making the safe one-argument form fully self-contained.
107
+ return pathArgValue(values, '--root') ? root : process.cwd();
108
+ }
92
109
  function userRuntimeRoot() {
93
110
  return path.join(os.homedir(), '.Newmark');
94
111
  }
@@ -174,18 +191,30 @@ function writableRuntimeRoot(candidate) {
174
191
  const explicitRoot = pathArgValue(args, '--root');
175
192
  const root = explicitRoot ? writableRuntimeRoot(explicitRoot) : userRuntimeRoot();
176
193
  if (isHelpArg) {
177
- console.log((0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
194
+ console.log(isFlow ? (0, cli_help_1.newmarkFlowHelpText)() : isEdit ? (0, cli_help_1.newmarkEditHelpText)() : (0, cli_help_1.newmarkHelpText)((0, installUpdate_1.currentAppVersion)()));
178
195
  process.exit(0);
179
196
  }
180
197
  if (isVersionArg) {
181
198
  console.log((0, installUpdate_1.currentAppVersion)());
182
199
  process.exit(0);
183
200
  }
184
- function firstRunInit(r) {
201
+ const unknownCommand = !hasCliCommand && !isTui && !isGui && !isCli && !isServer && !isFlow && !isEdit
202
+ ? (0, cli_discovery_1.unknownTopLevelCommand)(args)
203
+ : undefined;
204
+ if (unknownCommand) {
205
+ console.error(`Unknown Newmark command or argument: ${unknownCommand}. Run --help to see the supported entrypoints.`);
206
+ process.exit(2);
207
+ }
208
+ function firstRunInit(r, options = {}) {
185
209
  fs.mkdirSync(r, { recursive: true });
186
- migrateLegacyRuntimeRoot(r);
210
+ // Legacy AppData migration is only valid for the canonical user runtime.
211
+ // An explicit --root is an isolation boundary (tests, portable roots, and
212
+ // caller-selected workspaces) and must never inherit the user's sessions,
213
+ // archives, providers, or workspaces.
214
+ if (path.resolve(r) === path.resolve(userRuntimeRoot()))
215
+ migrateLegacyRuntimeRoot(r);
187
216
  const { ensureRootConfig } = require('./core/config');
188
- ensureRootConfig(r);
217
+ ensureRootConfig(r, options);
189
218
  if (!fs.existsSync(path.join(r, 'agent.md'))) {
190
219
  fs.writeFileSync(path.join(r, 'agent.md'), '# Newmark Agent\n\nYou are a powerful coding assistant.\n', 'utf-8');
191
220
  }
@@ -270,13 +299,13 @@ function launchGui() {
270
299
  console.error('Unable to locate the Newmark GUI runtime. Reinstall newmark-agent with optional dependencies enabled, or install a Newmark desktop package.');
271
300
  process.exit(1);
272
301
  }
273
- firstRunInit(root);
302
+ firstRunInit(root, { readOnly: isReadOnlyValidation });
274
303
  if (isGui) {
275
304
  launchGui();
276
305
  }
277
306
  else if (isTui) {
278
307
  const { start } = require('./tui/src/app');
279
- start({ root, workspacePath: process.cwd(), desktopDist: __dirname });
308
+ start({ root, workspacePath: resolveTuiWorkspacePath(args, root), desktopDist: __dirname });
280
309
  }
281
310
  else if (hasCliCommand) {
282
311
  (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,10 +88,10 @@ 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;