@xuda.io/ai_module 1.1.5659 → 1.1.5661

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/index.mjs CHANGED
@@ -37,7 +37,7 @@ const run_process = function (command, args, input, options = {}) {
37
37
  // leaves the native process orphaned (PPID 1), still holding its OpenAI
38
38
  // socket and running forever past the deadline. We instead run the child in
39
39
  // its own process group (detached) and kill the whole group ourselves.
40
- const { onStdout, onStderr, timeout, killSignal = 'SIGKILL', ...spawn_options } = options;
40
+ const { onStdout, onStderr, timeout, killSignal = 'SIGKILL', abort_check, abort_interval_ms, ...spawn_options } = options;
41
41
  const child = spawn(command, args, {
42
42
  stdio: ['pipe', 'pipe', 'pipe'],
43
43
  ...(timeout ? { detached: true } : {}),
@@ -66,6 +66,30 @@ const run_process = function (command, args, input, options = {}) {
66
66
  }, timeout)
67
67
  : null;
68
68
 
69
+ // A caller that can decide, part way through, that this process must stop. Used by the
70
+ // Xucode run tracker to end a run it has judged (plan 9.3 layer 6) without waiting for the
71
+ // timeout, which is the whole difference between minutes of stolen cycles and half an hour.
72
+ // It reuses kill_tree for the same reason the timeout does: signalling the direct child
73
+ // leaves the native binary running.
74
+ let aborted_reason = null;
75
+ const abort_timer = typeof abort_check === 'function'
76
+ ? setInterval(async () => {
77
+ try {
78
+ const reason = await abort_check();
79
+ if (!reason) return;
80
+ aborted_reason = String(reason);
81
+ stderr += `\n${aborted_reason}`;
82
+ kill_tree(killSignal);
83
+ } catch (e) {
84
+ // A judge that throws must never take down the process it is judging.
85
+ }
86
+ }, Number(abort_interval_ms) || 5000)
87
+ : null;
88
+ const clear_timers = () => {
89
+ if (timer) clearTimeout(timer);
90
+ if (abort_timer) clearInterval(abort_timer);
91
+ };
92
+
69
93
  child.stdout.on('data', (chunk) => {
70
94
  const text = chunk.toString();
71
95
  stdout += text;
@@ -83,12 +107,12 @@ const run_process = function (command, args, input, options = {}) {
83
107
  });
84
108
 
85
109
  child.on('error', (err) => {
86
- if (timer) clearTimeout(timer);
110
+ clear_timers();
87
111
  reject(err);
88
112
  });
89
113
  child.on('close', (exit_code) => {
90
- if (timer) clearTimeout(timer);
91
- resolve({ exit_code, stdout, stderr, timed_out });
114
+ clear_timers();
115
+ resolve({ exit_code, stdout, stderr, timed_out, aborted: !!aborted_reason, abort_reason: aborted_reason });
92
116
  });
93
117
 
94
118
  if (input) {
@@ -98,26 +122,28 @@ const run_process = function (command, args, input, options = {}) {
98
122
  });
99
123
  };
100
124
 
101
- // UI-228 (xudex). The runtime seam: every xudex layer asks this for a WORKSPACE and never learns
102
- // whether it got a VM, an ephemeral runner or a local working copy. See xudex_runtime.mjs and
103
- // docs/plans/xudex.md section 3.8. Instantiated here rather than inside that file because
125
+ // UI-228 (xucode). The runtime seam: every xucode layer asks this for a WORKSPACE and never learns
126
+ // whether it got a VM, an ephemeral runner or a local working copy. See xucode_runtime.mjs and
127
+ // docs/plans/xucode.md section 3.8. Instantiated here rather than inside that file because
104
128
  // run_process above is the only spawn wrapper whose detached process-group kill actually stops a
105
129
  // Codex run, and a second copy of that fix is the last thing this codebase needs.
106
- const { create_runtime: _create_xudex_runtime } = await import('./xudex_runtime.mjs');
107
- const { create_mirror: _create_xudex_mirror } = await import('./xudex_mirror.mjs');
130
+ const { create_runtime: _create_xucode_runtime } = await import('./xucode_runtime.mjs');
131
+ const { create_mirror: _create_xucode_mirror } = await import('./xucode_mirror.mjs');
108
132
  // The run tracker watches a workspace through the same handle everything else uses, so it
109
- // works identically on a VM, an ephemeral runner or a container. See xudex_tracker.mjs.
110
- const { create_run_tracker: create_xudex_run_tracker } = await import('./xudex_tracker.mjs');
133
+ // works identically on a VM, an ephemeral runner or a container. See xucode_tracker.mjs.
134
+ const { create_run_tracker: create_xucode_run_tracker } = await import('./xucode_tracker.mjs');
111
135
  // The engine registry: codex and Claude Code behind one launch/parse contract, so nothing above
112
- // this line branches on which CLI is running. See xudex_engines.mjs.
113
- const xudex_engines = await import('./xudex_engines.mjs');
114
- // The verify loop: what makes a xudex run different from a terminal that says "done" and
115
- // leaves your build broken. See xudex_verify.mjs.
116
- const xudex_verify = await import('./xudex_verify.mjs');
117
- // The last gate of that loop: is the app still actually running. See xudex_preview.mjs.
118
- const { create_preview: _create_xudex_preview } = await import('./xudex_preview.mjs');
119
- const xudex_preview = _create_xudex_preview({});
120
- const xudex_runtime = _create_xudex_runtime({ run_process });
136
+ // this line branches on which CLI is running. See xucode_engines.mjs.
137
+ const xucode_engines = await import('./xucode_engines.mjs');
138
+ // The verify loop: what makes a xucode run different from a terminal that says "done" and
139
+ // leaves your build broken. See xucode_verify.mjs.
140
+ const xucode_verify = await import('./xucode_verify.mjs');
141
+ // Where the provider credential lives. The machine gets a run token; this box keeps the key.
142
+ const xucode_proxy = await import('./xucode_proxy.mjs');
143
+ // The last gate of that loop: is the app still actually running. See xucode_preview.mjs.
144
+ const { create_preview: _create_xucode_preview, preview_host: xucode_preview_host } = await import('./xucode_preview.mjs');
145
+ const xucode_preview = _create_xucode_preview({});
146
+ const xucode_runtime = _create_xucode_runtime({ run_process });
121
147
 
122
148
  // UI-220. Same spawn, opposite lifetime: start the process, hand back its pid and walk away, with
123
149
  // stdout and stderr going straight to a file that never passes through this process. A code run
@@ -531,10 +557,10 @@ const bot_ms = await import(`${module_path}/bot_protection_module/index_ms.mjs`)
531
557
  // Owns whether the AI answers, and with what, on every channel. This module used to decide
532
558
  // it from flat fields on the profile doc; see auto_response() below.
533
559
  const auto_response_ms = await import(`${module_path}/auto_response_module/index_ms.mjs`);
534
- // UI-228: the `xudex_run` identity gate. Declared at level 2 in verify_policy and currently
560
+ // UI-228: the `xucode_run` identity gate. Declared at level 2 in verify_policy and currently
535
561
  // SHADOWED, so this reports and never denies until the platform enforcement floor moves.
536
562
  const verify_ms = await import(`${module_path}/verify_module/index_ms.mjs`);
537
- // UI-228: `xudex_touch`, which is what the idle clock that suspends a machine measures from.
563
+ // UI-228: `xucode_touch`, which is what the idle clock that suspends a machine measures from.
538
564
  // The ASYNC wrapper deliberately: nothing about a run should wait on a timestamp being written.
539
565
  const deploy_msa = await import(`${module_path}/deploy_module/index_msa.mjs`);
540
566
  // The SYNC twin: the VM substrate has to know which machine it got and what Proxmox answered,
@@ -543,7 +569,7 @@ const deploy_ms = await import(`${module_path}/deploy_module/index_ms.mjs`);
543
569
  // The entitlement rules themselves, imported as plain logic rather than called over the broker.
544
570
  // They are a pure function of the account document, so a queue round trip would buy nothing and
545
571
  // cost a hop on the path of every single run.
546
- const xudex_deploy_rules = await import(`${module_path}/deploy_module/xudex.mjs`);
572
+ const xucode_deploy_rules = await import(`${module_path}/deploy_module/xucode.mjs`);
547
573
 
548
574
  const ws_dashboard_msa = await import(`${module_path}/ws_dashboard_module/index_msa.mjs`);
549
575
  // Sync twin of the above: the chat-finished alert has to ASK whether the user is
@@ -3447,7 +3473,7 @@ export const get_code_run = async function (req) {
3447
3473
  repo_id: run.repo_id || null,
3448
3474
  branch: run.branch || null,
3449
3475
  base_sha: run.base_sha || null,
3450
- // UI-228: what the verification found. Present only on a xudex run, and the reason the
3476
+ // UI-228: what the verification found. Present only on a xucode run, and the reason the
3451
3477
  // panel can say "test failed" with the output instead of leaving the customer to guess
3452
3478
  // why the answer sounded confident and the build is red.
3453
3479
  verify: run.verify || null,
@@ -3686,7 +3712,7 @@ export const git_repo_list = async function (req) {
3686
3712
  };
3687
3713
 
3688
3714
  // ── UI-228: BYOK provider keys ─────────────────────────────────────────────────────────────
3689
- // docs/plans/xudex.md 5.2. Claude Code and every engine after it run on the CUSTOMER'S key, not
3715
+ // docs/plans/xucode.md 5.2. Claude Code and every engine after it run on the CUSTOMER'S key, not
3690
3716
  // ours. That removes model cost from our books entirely and turns efficiency work into their
3691
3717
  // saving rather than our margin, which is a better product and an honest one.
3692
3718
  //
@@ -3694,40 +3720,40 @@ export const git_repo_list = async function (req) {
3694
3720
  // doc in the ACCOUNT'S project database, never on the app doc in xuda_master, because control DBs
3695
3721
  // replicate bidirectionally fleet-wide through the master hub and a secret there would be copied
3696
3722
  // to every region. No read path returns more than the last four characters.
3697
- const XUDEX_KEY_PROVIDERS = ['anthropic', 'openai'];
3723
+ const XUCODE_KEY_PROVIDERS = ['anthropic', 'openai'];
3698
3724
 
3699
3725
  // What a key looks like, checked only enough to catch a paste that obviously is not one. This is
3700
3726
  // deliberately not strict: providers change their prefixes, and refusing a valid key is worse than
3701
3727
  // accepting an invalid one that fails clearly on first use.
3702
- const xudex_key_shape_error = function (provider, api_key) {
3728
+ const xucode_key_shape_error = function (provider, api_key) {
3703
3729
  const key = String(api_key || '').trim();
3704
3730
  if (key.length < 20) return 'That does not look like an API key. Paste the whole key.';
3705
3731
  if (provider === 'anthropic' && !key.startsWith('sk-ant-')) {
3706
3732
  // The single most common wrong thing to paste, so it is worth naming: a Claude Pro or Max
3707
3733
  // SUBSCRIPTION cannot drive Claude Code on our machines, whatever it can do on a laptop.
3708
3734
  // Saying it here costs one line and saves a support conversation with every team that tries.
3709
- return 'That does not look like an Anthropic API key (they start with sk-ant-). A Claude Pro or Max subscription will not work here: Xudex needs an API key with credits on it, from console.anthropic.com.';
3735
+ return 'That does not look like an Anthropic API key (they start with sk-ant-). A Claude Pro or Max subscription will not work here: Xucode needs an API key with credits on it, from console.anthropic.com.';
3710
3736
  }
3711
3737
  if (provider === 'openai' && !key.startsWith('sk-')) return 'That does not look like an OpenAI API key (they start with sk-).';
3712
3738
  return null;
3713
3739
  };
3714
3740
 
3715
- const xudex_key_safe = function (doc) {
3741
+ const xucode_key_safe = function (doc) {
3716
3742
  if (!doc) return null;
3717
3743
  return { provider: doc.provider, last4: doc.key_last4 || null, added_by_uid: doc.added_by_uid, ts: doc.ts };
3718
3744
  };
3719
3745
 
3720
- export const xudex_key_set = async function (req) {
3746
+ export const xucode_key_set = async function (req) {
3721
3747
  const { uid, profile_id, provider, api_key } = req;
3722
3748
  try {
3723
- if (!XUDEX_KEY_PROVIDERS.includes(provider)) return { code: -1, data: 'that provider is not supported' };
3724
- const shape_error = xudex_key_shape_error(provider, api_key);
3749
+ if (!XUCODE_KEY_PROVIDERS.includes(provider)) return { code: -1, data: 'that provider is not supported' };
3750
+ const shape_error = xucode_key_shape_error(provider, api_key);
3725
3751
  if (shape_error) return { code: -1, data: shape_error };
3726
3752
 
3727
3753
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
3728
3754
  // One key per provider per account: adding a new one replaces the old rather than stacking,
3729
3755
  // so there is never a question of which of two keys a run used.
3730
- const existing = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xudex_key', provider, stat: 3 }, limit: 5 });
3756
+ const existing = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 5 });
3731
3757
  for (const doc of existing?.docs || []) {
3732
3758
  doc.stat = 4;
3733
3759
  doc.ts = Date.now();
@@ -3735,8 +3761,8 @@ export const xudex_key_set = async function (req) {
3735
3761
  }
3736
3762
 
3737
3763
  const key_doc = {
3738
- _id: await _common.xuda_get_uuid('xudex_key'),
3739
- docType: 'xudex_key',
3764
+ _id: await _common.xuda_get_uuid('xucode_key'),
3765
+ docType: 'xucode_key',
3740
3766
  stat: 3,
3741
3767
  provider,
3742
3768
  api_key: String(api_key).trim(),
@@ -3746,29 +3772,29 @@ export const xudex_key_set = async function (req) {
3746
3772
  ts: Date.now(),
3747
3773
  };
3748
3774
  await db_module.save_app_couch_doc_native(account_profile_info.app_id, key_doc);
3749
- return { code: 1, data: { key: xudex_key_safe(key_doc) } };
3775
+ return { code: 1, data: { key: xucode_key_safe(key_doc) } };
3750
3776
  } catch (err) {
3751
- console.error(`[xudex] key set failed: ${err?.message || err}`);
3777
+ console.error(`[xucode] key set failed: ${err?.message || err}`);
3752
3778
  return { code: -1, data: 'could not save that key' };
3753
3779
  }
3754
3780
  };
3755
3781
 
3756
- export const xudex_key_list = async function (req) {
3782
+ export const xucode_key_list = async function (req) {
3757
3783
  const { uid, profile_id } = req;
3758
3784
  try {
3759
3785
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
3760
- const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xudex_key', stat: 3 }, limit: 20 });
3761
- return { code: 1, data: { keys: (q?.docs || []).map(xudex_key_safe) } };
3786
+ const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', stat: 3 }, limit: 20 });
3787
+ return { code: 1, data: { keys: (q?.docs || []).map(xucode_key_safe) } };
3762
3788
  } catch (err) {
3763
3789
  return { code: -1, data: 'could not read your keys' };
3764
3790
  }
3765
3791
  };
3766
3792
 
3767
- export const xudex_key_delete = async function (req) {
3793
+ export const xucode_key_delete = async function (req) {
3768
3794
  const { uid, profile_id, provider } = req;
3769
3795
  try {
3770
3796
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
3771
- const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xudex_key', provider, stat: 3 }, limit: 5 });
3797
+ const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 5 });
3772
3798
  let removed = 0;
3773
3799
  for (const doc of q?.docs || []) {
3774
3800
  doc.stat = 4;
@@ -3783,12 +3809,26 @@ export const xudex_key_delete = async function (req) {
3783
3809
  }
3784
3810
  };
3785
3811
 
3786
- // Internal: the key itself, for a run that is about to start. The only path that reads it, and it
3787
- // never leaves this process except as an environment variable on the engine's own child.
3788
- const xudex_resolve_api_key = async function (uid, profile_id, provider) {
3812
+ // Does this account have its own key for a provider, and WHERE does it live. Deliberately asks for
3813
+ // `_id` only: the run path no longer needs to see a customer key at all when the proxy is on, and
3814
+ // the safest way to keep a secret out of a process is not to load it. The app id goes into the run
3815
+ // token so the proxy can do the read on our side.
3816
+ const xucode_key_ref = async function (uid, profile_id, provider) {
3789
3817
  try {
3790
3818
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
3791
- const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xudex_key', provider, stat: 3 }, limit: 1 });
3819
+ const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 1, fields: ['_id'] });
3820
+ return { app_id: account_profile_info.app_id, has_key: !!q?.docs?.length };
3821
+ } catch (err) {
3822
+ return { app_id: null, has_key: false };
3823
+ }
3824
+ };
3825
+
3826
+ // Internal: the key itself, for a run that is about to start. Only reached when the proxy is OFF,
3827
+ // which is the one case where the engine still needs the key in its own environment.
3828
+ const xucode_resolve_api_key = async function (uid, profile_id, provider) {
3829
+ try {
3830
+ const account_profile_info = await get_active_account_profile_info(uid, profile_id);
3831
+ const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 1 });
3792
3832
  return q?.docs?.[0]?.api_key || null;
3793
3833
  } catch (err) {
3794
3834
  return null;
@@ -3796,24 +3836,124 @@ const xudex_resolve_api_key = async function (uid, profile_id, provider) {
3796
3836
  };
3797
3837
 
3798
3838
  // What the run path asks for: everything needed to launch, or a refusal a customer can act on.
3799
- // Kept here rather than in xudex_engines.mjs because only this file can read the key store.
3839
+ // Kept here rather than in xucode_engines.mjs because only this file can read the key store.
3800
3840
  //
3801
3841
  // DELIBERATELY NOT EXPORTED. Its return value contains `launch.env`, which holds the customer's
3802
3842
  // provider API key, and every `export const` in this module gets a generated queue wrapper and is
3803
3843
  // therefore callable over the broker. Exporting this would put a customer's key one internal
3804
3844
  // broker call away from anybody, for no benefit at all: the only caller is the run path, which
3805
3845
  // lives in this same file.
3806
- const xudex_prepare_engine = async function ({ uid, profile_id, engine, model, resume_session_id, sandbox } = {}) {
3807
- const descriptor = xudex_engines.get_engine(engine);
3846
+ const xucode_prepare_engine = async function ({ uid, profile_id, engine, model, resume_session_id, sandbox, run_id, project_id } = {}) {
3847
+ const descriptor = xucode_engines.get_engine(engine);
3808
3848
  if (!descriptor) return { code: -1, data: 'That engine is not available.' };
3809
- const api_key = descriptor.byok ? await xudex_resolve_api_key(uid, profile_id, descriptor.provider) : null;
3810
- const launch = xudex_engines.prepare_launch({ engine, model, resume_session_id, api_key, sandbox });
3849
+
3850
+ // Does this account have its own key for this provider. Asked WITHOUT reading it, because with
3851
+ // the proxy on nothing in this process needs to see one.
3852
+ const key_ref = await xucode_key_ref(uid, profile_id, descriptor.provider);
3853
+
3854
+ // A BYOK engine with no key is refused HERE, before a workspace is prepared or a machine woken.
3855
+ // The check moved up from prepare_launch because a proxied launch legitimately carries no key,
3856
+ // so "no key in hand" stopped being the same question as "this account cannot run this engine".
3857
+ if (descriptor.byok && !key_ref.has_key) {
3858
+ return {
3859
+ code: -1,
3860
+ data: `${descriptor.label} runs on your own API key. Add an ${descriptor.provider === 'anthropic' ? 'Anthropic' : descriptor.provider} API key in settings, then try again.`,
3861
+ needs_key: descriptor.provider,
3862
+ };
3863
+ }
3864
+
3865
+ // ── Where the credential lives ────────────────────────────────────────────────────────────
3866
+ // NO provider key reaches the machine, whoever owns it. The machine gets a signed run token and
3867
+ // the region server attaches the real credential: the customer's own when they have one for this
3868
+ // provider, ours otherwise (xucode_proxy.mjs).
3869
+ //
3870
+ // BYOK rides the same path deliberately. The first argument for the proxy was that OUR shared
3871
+ // key must not sit on a box running AI-written code, but a customer's key on that same box is
3872
+ // the same exposure with a different victim, and "they chose it" is a thin defence when the
3873
+ // machine is ours.
3874
+ const pconf = _conf.xucode?.proxy || {};
3875
+ const secret = xucode_proxy.proxy_secret();
3876
+ if (pconf.enabled !== false && secret) {
3877
+ const ttl = (Number(_conf.xucode?.run_timeout_ms) || 30 * 60 * 1000) + (Number(pconf.token_ttl_slack_ms) || 15 * 60 * 1000);
3878
+ const minted = xucode_proxy.mint_token({
3879
+ run_id,
3880
+ uid,
3881
+ project_id,
3882
+ engine: descriptor.key,
3883
+ provider: descriptor.provider,
3884
+ // Present means "this run spends the customer's own account". Absent means ours.
3885
+ key_app_id: key_ref.has_key ? key_ref.app_id : null,
3886
+ ttl_ms: ttl,
3887
+ secret,
3888
+ });
3889
+ if (minted.error) return { code: -1, data: 'Xuda could not prepare a credential for this run.' };
3890
+ const base = pconf.base_url || `https://${process.env.XUDA_HOSTNAME || _conf.domain}${pconf.prefix || '/cpi/xucode_proxy'}`;
3891
+ const proxy = { base_url: base, token: minted.token, home: `${pconf.engine_home_root || '/srv/xucode/.engine'}/${run_id}` };
3892
+ const launch = xucode_engines.prepare_launch({ engine, model, resume_session_id, sandbox, proxy });
3893
+ if (launch.error) return { code: -1, data: launch.error, needs_key: launch.needs_key || null };
3894
+ return { code: 1, launch, has_key: key_ref.has_key, proxied: true, byok: key_ref.has_key };
3895
+ }
3896
+
3897
+ // Proxy off. The key travels to the machine in the engine's environment, which is correct when
3898
+ // the substrate IS this server and a real exposure anywhere else, so it is said out loud rather
3899
+ // than happening quietly.
3900
+ const api_key = key_ref.has_key ? await xucode_resolve_api_key(uid, profile_id, descriptor.provider) : descriptor.provider === 'openai' ? _conf.OPENAI_API_KEY || null : null;
3901
+ console.warn(`[xucode] proxy disabled: ${descriptor.key} will run with ${key_ref.has_key ? "the customer's" : 'the platform'} key in its environment`);
3902
+ const launch = xucode_engines.prepare_launch({ engine, model, resume_session_id, api_key, sandbox });
3811
3903
  if (launch.error) return { code: -1, data: launch.error, needs_key: launch.needs_key || null };
3812
- return { code: 1, launch, has_key: !!api_key };
3904
+ return { code: 1, launch, has_key: !!api_key, proxied: false, byok: key_ref.has_key };
3813
3905
  };
3814
3906
 
3815
- export const xudex_engine_list = async function () {
3816
- return { code: 1, data: { engines: xudex_engines.list_engines() } };
3907
+ // What the Code tab shows for a project's preview (plan 7).
3908
+ //
3909
+ // Deliberately does NOT wake a sleeping machine. A panel that polls would otherwise keep every
3910
+ // machine awake forever, which is the opposite of what sleep is for and would bill the customer
3911
+ // for a screen they left open. So a sleeping machine reports 'asleep' with its URL, and the next
3912
+ // run wakes it in the normal way.
3913
+ //
3914
+ // The URL is derived rather than stored, so it is always the one the router will resolve; there is
3915
+ // no second copy to drift. `listening` is the only part that needs the machine, and it is asked
3916
+ // for only when the machine is already up.
3917
+ export const xucode_preview_status = async function (req) {
3918
+ const { uid, profile_id, app_id } = req || {};
3919
+ try {
3920
+ if (!app_id) return { code: -1, data: 'app_id is required' };
3921
+ const host = xucode_preview_host(app_id);
3922
+ const port = xucode_preview.preview_port(app_id);
3923
+ const base = { url: host ? `https://${host}/` : null, port };
3924
+
3925
+ // `no_wake`: a status read must never resume a machine, or a panel left open keeps every
3926
+ // machine awake and bills the customer for a screen nobody is watching.
3927
+ const machine = await deploy_ms.xucode_machine_for({ uid, app_id, no_wake: true }).catch(() => null);
3928
+ if (!machine || machine.code < 0) {
3929
+ // Asleep, quarantined and "no machine at all" are three different answers, and the customer
3930
+ // deserves the right one: only the middle is a fault.
3931
+ const state = machine?.asleep ? 'asleep' : machine?.quarantined ? 'quarantined' : 'unavailable';
3932
+ return { code: 1, data: { ...base, state, why: String(machine?.data || 'this project has no machine yet') } };
3933
+ }
3934
+
3935
+ // The machine is up (xucode_machine_for refuses a quarantined one and wakes nothing that is
3936
+ // merely asleep beyond its own resume path), so it is safe to ask whether anything is serving.
3937
+ const dir = path.join(_conf.xucode?.projects_root || '/srv/xucode', String(app_id));
3938
+ let listening = false;
3939
+ try {
3940
+ const workspace = await xucode_runtime.acquire({ uid, project_id: app_id, app_id, dir });
3941
+ const status = await xucode_preview.status(workspace, app_id, port);
3942
+ listening = status?.listening === true;
3943
+ await workspace.release().catch(() => {});
3944
+ } catch (e) {
3945
+ return { code: 1, data: { ...base, state: 'unavailable', why: 'Xuda could not reach this project just now.' } };
3946
+ }
3947
+
3948
+ return { code: 1, data: { ...base, state: listening ? 'running' : 'stopped' } };
3949
+ } catch (err) {
3950
+ console.error(`[xucode] preview status failed: ${err?.message || err}`);
3951
+ return { code: -1, data: 'could not read the preview' };
3952
+ }
3953
+ };
3954
+
3955
+ export const xucode_engine_list = async function () {
3956
+ return { code: 1, data: { engines: xucode_engines.list_engines() } };
3817
3957
  };
3818
3958
 
3819
3959
  export const git_repo_disconnect = async function (req) {
@@ -3912,25 +4052,25 @@ const ensure_git_working_copy = async function ({ uid, repo, conversation_id })
3912
4052
  };
3913
4053
 
3914
4054
  // ── UI-228: the bare mirror ────────────────────────────────────────────────────────────────
3915
- // See xudex_mirror.mjs for why this exists: it is what lets a working copy live on a machine that
4055
+ // See xucode_mirror.mjs for why this exists: it is what lets a working copy live on a machine that
3916
4056
  // never holds the customer's token. git_exec is handed over rather than re-implemented, because it is
3917
4057
  // the only thing here that knows how to give git a credential without it landing in an argv.
3918
4058
  // UI-228: the VM substrate, registered as the second implementation of the runtime interface.
3919
- // It reaches the machine over the Proxmox guest agent rather than SSH, because a xudex box is
3920
- // app_type 'vps' and customer VPS on this platform are keyless by design. See xudex_vm.mjs.
4059
+ // It reaches the machine over the Proxmox guest agent rather than SSH, because a xucode box is
4060
+ // app_type 'vps' and customer VPS on this platform are keyless by design. See xucode_vm.mjs.
3921
4061
  //
3922
4062
  // `resolve_machine` and the Proxmox client are injected rather than imported: only deploy_module
3923
4063
  // owns Proxmox, and asking it over the broker keeps that ownership where it belongs.
3924
- const { create_vm_substrate: _create_xudex_vm } = await import('./xudex_vm.mjs');
3925
- xudex_runtime.register(
3926
- _create_xudex_vm({
4064
+ const { create_vm_substrate: _create_xucode_vm } = await import('./xucode_vm.mjs');
4065
+ xucode_runtime.register(
4066
+ _create_xucode_vm({
3927
4067
  pve_request: async (node_doc, method, url, body) => {
3928
4068
  const ret = await deploy_ms.proxmox_api_request({ node_doc, method, url, body });
3929
4069
  if (!ret || ret.code < 0) throw new Error(ret?.data || 'proxmox request failed');
3930
4070
  return ret.data;
3931
4071
  },
3932
4072
  resolve_machine: async ({ uid, app_id }) => {
3933
- const ret = await deploy_ms.xudex_machine_for({ uid, app_id });
4073
+ const ret = await deploy_ms.xucode_machine_for({ uid, app_id });
3934
4074
  return ret?.code > 0 ? ret.data : null;
3935
4075
  },
3936
4076
  }),
@@ -3943,28 +4083,28 @@ xudex_runtime.register(
3943
4083
  //
3944
4084
  // A substrate that is registered but not available is the normal case, not a fault: `local` is
3945
4085
  // gated off outside dev on purpose (plan 3.1), and `vm` is unavailable on a box with no Proxmox.
3946
- console.log(`[xudex] runtime substrates: ${xudex_runtime.list().map((s) => `${s.kind}=${s.available ? 'available' : 'off'}`).join(' ') || 'none'}`);
4086
+ console.log(`[xucode] runtime substrates: ${xucode_runtime.list().map((s) => `${s.kind}=${s.available ? 'available' : 'off'}`).join(' ') || 'none'}`);
3947
4087
 
3948
- const xudex_mirror = _create_xudex_mirror({
4088
+ const xucode_mirror = _create_xucode_mirror({
3949
4089
  git_exec,
3950
4090
  du_mb: async (dir) =>
3951
4091
  Number(((await run_process('bash', ['-lc', `du -sm ${JSON.stringify(dir)} | cut -f1`], null, { timeout: 60000 })).stdout || '').trim()) || 0,
3952
- mirror_root: () => _conf.xudex?.mirror_path || path.join(git_repos_root(), '_mirrors'),
4092
+ mirror_root: () => _conf.xucode?.mirror_path || path.join(git_repos_root(), '_mirrors'),
3953
4093
  clone_timeout_ms: GIT_CLONE_TIMEOUT_MS,
3954
4094
  clone_max_mb: GIT_CLONE_MAX_MB,
3955
4095
  });
3956
4096
 
3957
4097
  // UI-228: the whole chain in one place. Deliberately constructed HERE rather than beside the
3958
- // other xudex imports at the top of the file: it needs `xudex_mirror` and `git_exec`, both of
4098
+ // other xucode imports at the top of the file: it needs `xucode_mirror` and `git_exec`, both of
3959
4099
  // which are defined further down, and a top-level const that reads them earlier is a temporal
3960
4100
  // dead zone error that takes the whole module down at boot. `node --check` does not catch it.
3961
- const { create_runner: _create_xudex_runner } = await import('./xudex_run.mjs');
3962
- const xudex_runner = _create_xudex_runner({
3963
- runtime: xudex_runtime,
3964
- mirror: xudex_mirror,
3965
- engines: xudex_engines,
3966
- verify: xudex_verify,
3967
- tracker_factory: ({ workspace }) => create_xudex_run_tracker({ workspace }),
4101
+ const { create_runner: _create_xucode_runner } = await import('./xucode_run.mjs');
4102
+ const xucode_runner = _create_xucode_runner({
4103
+ runtime: xucode_runtime,
4104
+ mirror: xucode_mirror,
4105
+ engines: xucode_engines,
4106
+ verify: xucode_verify,
4107
+ tracker_factory: ({ workspace }) => create_xucode_run_tracker({ workspace }),
3968
4108
  run_process,
3969
4109
  git_exec,
3970
4110
  });
@@ -3976,54 +4116,54 @@ const xudex_runner = _create_xudex_runner({
3976
4116
  // The monthly cap from the plan (50 on free, unlimited above it). Counted from the tracker's own
3977
4117
  // records, which is the second of the three jobs section 9.4 gives that component: one meter, not
3978
4118
  // a separate counter that can drift away from what actually ran.
3979
- const xudex_runs_this_month = async function (app_db_id) {
4119
+ const xucode_runs_this_month = async function (app_db_id) {
3980
4120
  const since = new Date();
3981
4121
  since.setUTCDate(1);
3982
4122
  since.setUTCHours(0, 0, 0, 0);
3983
4123
  try {
3984
- const q = await db_module.find_app_couch_query(app_db_id, { selector: { docType: 'xudex_run', ts: { $gte: since.getTime() } }, limit: 1000 });
4124
+ const q = await db_module.find_app_couch_query(app_db_id, { selector: { docType: 'xucode_run', ts: { $gte: since.getTime() } }, limit: 1000 });
3985
4125
  return (q?.docs || []).length;
3986
4126
  } catch (err) {
3987
4127
  // A failed count must not block a paying customer's work. Erring toward letting the run
3988
4128
  // happen is the right way round: the cap protects margin, and the abuse layers protect the
3989
4129
  // thing that actually matters.
3990
- console.warn(`[xudex] run count failed: ${err.message}`);
4130
+ console.warn(`[xucode] run count failed: ${err.message}`);
3991
4131
  return 0;
3992
4132
  }
3993
4133
  };
3994
4134
 
3995
- export const xudex_run = async function (req) {
4135
+ export const xucode_run = async function (req) {
3996
4136
  const { uid, profile_id, app_id, conversation_id, prompt, engine, model } = req;
3997
4137
  try {
3998
- if (_conf.xudex?.enabled !== true) return { code: -1, data: 'Xudex is not available yet.' };
4138
+ if (_conf.xucode?.enabled !== true) return { code: -1, data: 'Xucode is not available yet.' };
3999
4139
  if (!app_id) return { code: -1, data: 'app_id is required' };
4000
4140
  if (!prompt || !String(prompt).trim()) return { code: -1, data: 'There is nothing to do: the request was empty.' };
4001
4141
 
4002
- // 1. Entitlement. The same rules deploy_xudex uses to hand out a machine, asked again here,
4142
+ // 1. Entitlement. The same rules deploy_xucode uses to hand out a machine, asked again here,
4003
4143
  // because a membership can lapse between provisioning a machine and using it.
4004
4144
  //
4005
4145
  // Asked BEFORE resolving the profile's project database, deliberately. That lookup throws on
4006
4146
  // an account it cannot find, and a throw lands in the catch at the bottom as "please try
4007
4147
  // again", which is the least useful thing we could say to someone whose real problem is that
4008
- // their membership does not include Xudex. Cheap checks first, and each with its own answer.
4148
+ // their membership does not include Xucode. Cheap checks first, and each with its own answer.
4009
4149
  let account_doc = null;
4010
4150
  try {
4011
4151
  const acct = await db_module.get_couch_doc('xuda_accounts', uid);
4012
4152
  account_doc = acct?.code > -1 ? acct.data : null;
4013
4153
  } catch (e) {}
4014
4154
  if (!account_doc) return { code: -1, data: 'Xuda could not read your account just now. Please try again.' };
4015
- const ent = xudex_deploy_rules.xudex_entitlement(account_doc);
4155
+ const ent = xucode_deploy_rules.xucode_entitlement(account_doc);
4016
4156
  if (!ent.allowed) return { code: -1, data: ent.reason, needs_membership: true };
4017
4157
 
4018
- // 2. The identity gate. It is SHADOWED today (verify_policy declares xudex_run at level 2 and
4158
+ // 2. The identity gate. It is SHADOWED today (verify_policy declares xucode_run at level 2 and
4019
4159
  // the platform enforcement floor is above it), so this reports what it would have refused and
4020
4160
  // lets the run proceed. When the floor moves, this line starts denying without being touched.
4021
4161
  try {
4022
- const gate = await verify_ms.verify_gate_check_for_uid({ data: { uid, product: 'xudex_run' } });
4162
+ const gate = await verify_ms.verify_gate_check_for_uid({ data: { uid, product: 'xucode_run' } });
4023
4163
  if (gate?.code > 0 && gate.data && gate.data.allow === false) {
4024
4164
  return {
4025
4165
  code: -412,
4026
- data: gate.data.message || 'Identity verification is required before running code on Xudex.',
4166
+ data: gate.data.message || 'Identity verification is required before running code on Xucode.',
4027
4167
  error: 'id_verification_required',
4028
4168
  required_level: gate.data.required_level,
4029
4169
  level: gate.data.level,
@@ -4032,7 +4172,7 @@ export const xudex_run = async function (req) {
4032
4172
  }
4033
4173
  } catch (e) {
4034
4174
  // A verification service that is down must not stop paying customers working.
4035
- console.warn(`[xudex] gate check failed, allowing: ${e.message}`);
4175
+ console.warn(`[xucode] gate check failed, allowing: ${e.message}`);
4036
4176
  }
4037
4177
 
4038
4178
  // 3. The monthly cap. The project database is resolved here, at the first point that actually
@@ -4040,13 +4180,13 @@ export const xudex_run = async function (req) {
4040
4180
  const account_profile_info = await get_active_account_profile_info(uid, profile_id);
4041
4181
  const cap = Number(ent.flags?.runs_per_month);
4042
4182
  if (Number.isFinite(cap) && cap > 0) {
4043
- const used = await xudex_runs_this_month(account_profile_info.app_id);
4183
+ const used = await xucode_runs_this_month(account_profile_info.app_id);
4044
4184
  if (used >= cap) {
4045
- return { code: -1, data: `You have used all ${cap} Xudex runs on your plan this month. They reset at the start of next month, or you can upgrade for unlimited runs.`, cap_reached: true, used, cap };
4185
+ return { code: -1, data: `You have used all ${cap} Xucode runs on your plan this month. They reset at the start of next month, or you can upgrade for unlimited runs.`, cap_reached: true, used, cap };
4046
4186
  }
4047
4187
  }
4048
4188
 
4049
- // 4. The repository. A xudex run without one has nothing to work on, and saying so is better
4189
+ // 4. The repository. A xucode run without one has nothing to work on, and saying so is better
4050
4190
  // than starting an engine in an empty directory and letting it improvise.
4051
4191
  const repo_list = await git_repo_list({ uid, profile_id, app_id });
4052
4192
  const repo_ref = repo_list?.data?.repos?.[0];
@@ -4056,12 +4196,17 @@ export const xudex_run = async function (req) {
4056
4196
 
4057
4197
  // 5. The engine, and its key. A BYOK engine with no key is refused here, before a workspace is
4058
4198
  // prepared or a machine is woken.
4059
- const prepared = await xudex_prepare_engine({ uid, profile_id, engine, model, resume_session_id: req.resume_session_id || null });
4199
+ //
4200
+ // The run id is minted HERE rather than at the end with the record, because the proxy token
4201
+ // is scoped to it: the id has to exist before anything can be authorized against it. It goes
4202
+ // onto the record too, so a line in the proxy log and a run in the database are the same run.
4203
+ const run_id = await _common.xuda_get_uuid('xucode_run');
4204
+ const prepared = await xucode_prepare_engine({ uid, profile_id, engine, model, resume_session_id: req.resume_session_id || null, run_id, project_id: app_id });
4060
4205
  if (prepared.code < 0) return { code: -1, data: prepared.data, needs_key: prepared.needs_key || null };
4061
4206
 
4062
4207
  // 6. Run it.
4063
4208
  const emit = typeof req.on_event === 'function' ? req.on_event : null;
4064
- const ret = await xudex_runner.run({
4209
+ const ret = await xucode_runner.run({
4065
4210
  uid,
4066
4211
  repo: repo_full.repo,
4067
4212
  project_id: app_id,
@@ -4070,32 +4215,55 @@ export const xudex_run = async function (req) {
4070
4215
  launch: prepared.launch,
4071
4216
  emit,
4072
4217
  max_repair_attempts: req.max_repair_attempts,
4073
- verify_ctx: await xudex_verify_context(app_id),
4218
+ // A factory, not a value: the project's files live on whichever machine the runner ends up
4219
+ // acquiring, so this can only be answered once that machine exists.
4220
+ verify_ctx_factory: (workspace) => xucode_verify_context_for(workspace, app_id),
4074
4221
  });
4075
4222
 
4076
4223
  // 7. The record. It is the run meter, the abuse evidence and the benchmark sample all at once,
4077
4224
  // so it is written whether the run succeeded or not.
4078
4225
  if (ret.record) {
4079
4226
  try {
4080
- await db_module.save_app_couch_doc_native(account_profile_info.app_id, { _id: await _common.xuda_get_uuid('xudex_run'), ...ret.record, app_id, uid });
4227
+ await db_module.save_app_couch_doc_native(account_profile_info.app_id, { _id: run_id, ...ret.record, app_id, uid });
4081
4228
  } catch (e) {
4082
- console.error(`[xudex] could not persist the run record: ${e.message}`);
4229
+ console.error(`[xucode] could not persist the run record: ${e.message}`);
4083
4230
  }
4084
4231
  if (ret.record.verdict === 'flag') {
4085
- // Log-only until the thresholds have been tuned against real builds (9.4). Loud, because
4086
- // this is the line somebody will grep for when an abuse complaint arrives.
4087
- console.warn(`[xudex] RUN FLAGGED uid=${uid} app=${app_id} reasons=${(ret.record.reasons || []).join('; ')} enforced=${ret.record.enforced}`);
4232
+ // Loud either way, because this is the line somebody will grep for when an abuse
4233
+ // complaint arrives. `enforced` says whether anything was actually done about it, which
4234
+ // is a separate question and stays configurable (9.4).
4235
+ console.warn(`[xucode] RUN FLAGGED uid=${uid} app=${app_id} reasons=${(ret.record.reasons || []).join('; ')} enforced=${ret.record.enforced}`);
4236
+ }
4237
+
4238
+ // Layer 6 (9.3). The run has already been stopped by this point; this is what stops the
4239
+ // NEXT one, which is the half that actually ends an abuser rather than inconveniencing
4240
+ // them. Only ever reached with `tracker.enforce` on, so the default remains observe.
4241
+ if (ret.record.enforced === true) {
4242
+ try {
4243
+ const q = await deploy_msa.xucode_quarantine({ app_id, uid, reasons: ret.record.reasons || [], run_id });
4244
+ if (q?.code < 0) console.error(`[xucode] quarantine failed for ${app_id}: ${q.data}`);
4245
+ } catch (e) {
4246
+ console.error(`[xucode] quarantine threw for ${app_id}: ${e?.message || e}`);
4247
+ }
4088
4248
  }
4089
4249
  }
4090
4250
 
4091
- // 8. Keep the machine awake for as long as it is being used.
4092
- try {
4093
- await deploy_msa.xudex_touch({ app_id });
4094
- } catch (e) {}
4251
+ // 8. Keep the machine awake for as long as it is being used. Skipped for a run we just
4252
+ // stopped: touching it would reset the idle clock on a machine we want left alone.
4253
+ if (!ret.killed && ret.record?.enforced !== true) {
4254
+ try {
4255
+ // `uid` matters here: `app_id` is the PROJECT, and touch operates on the MACHINE. Passing
4256
+ // the project alone is why this silently did nothing on every run until now.
4257
+ const touched = await deploy_msa.xucode_touch({ app_id, uid });
4258
+ if (touched?.code < 0) console.warn(`[xucode] could not refresh activity for ${app_id}: ${touched.data}`);
4259
+ } catch (e) {
4260
+ console.warn(`[xucode] touch threw for ${app_id}: ${e?.message || e}`);
4261
+ }
4262
+ }
4095
4263
 
4096
4264
  return ret;
4097
4265
  } catch (err) {
4098
- console.error(`[xudex] run failed: ${err?.message || err}`);
4266
+ console.error(`[xucode] run failed: ${err?.message || err}`);
4099
4267
  return { code: -1, data: 'That run could not be completed. Please try again.' };
4100
4268
  }
4101
4269
  };
@@ -4103,31 +4271,36 @@ export const xudex_run = async function (req) {
4103
4271
  // What the verify loop needs to know about the project: its manifest, its top-level files and
4104
4272
  // whether dependencies are installed. Read from the working copy rather than assumed, because a
4105
4273
  // project whose package.json changed between runs plans different gates.
4106
- const xudex_verify_context = async function (project_id) {
4107
- const dir = path.join(_conf.xudex?.projects_root || '/srv/xudex', String(project_id));
4274
+ // Read from the WORKSPACE, not from this server's disk.
4275
+ //
4276
+ // This is the clone bug again, one layer up, and it is worth naming because it is the mistake
4277
+ // this architecture invites. Reading `/srv/xucode/<project>` locally was correct while runs
4278
+ // happened on the region server; once the workspace moved onto a machine, that path is simply
4279
+ // absent here, the read throws, and the catch below hands the verify loop an empty project. The
4280
+ // visible symptom is the worst kind: a run reporting "nothing to verify" over a repository with
4281
+ // a perfectly good test script, which is a verdict nobody should trust.
4282
+ //
4283
+ // Anything that needs to look at the project's files has to ask the workspace, because the
4284
+ // workspace is the only thing that knows which computer they are on.
4285
+ const xucode_verify_context_for = async function (workspace, project_id) {
4108
4286
  try {
4109
- const files = await fs.promises.readdir(dir);
4287
+ const files = await workspace.list_dir('.');
4110
4288
  let manifest = null;
4111
4289
  if (files.includes('package.json')) {
4112
4290
  try {
4113
- manifest = JSON.parse(await fs.promises.readFile(path.join(dir, 'package.json'), 'utf8'));
4291
+ manifest = JSON.parse(await workspace.read_file('package.json'));
4114
4292
  } catch (e) {
4115
- // A malformed package.json is the project's problem to fix, and the verify loop will say
4116
- // so through whatever gate trips on it. It is not a reason to refuse the run.
4117
- console.warn(`[xudex] unreadable package.json in ${project_id}`);
4293
+ console.warn(`[xucode] unreadable package.json in ${project_id}`);
4118
4294
  }
4119
4295
  }
4120
4296
  return {
4121
4297
  manifest,
4122
4298
  files,
4123
4299
  node_modules_present: files.includes('node_modules'),
4124
- // The runtime gate. Built here rather than inside the verify loop because only this layer
4125
- // knows which project it is and how to reach its dev server; the loop just calls it and
4126
- // treats a dead app like any other failing step, which means a broken preview goes back to
4127
- // the engine through the same repair path as a broken test.
4128
- runtime_check: xudex_preview.runtime_check({ project_id, manifest, package_manager: detect_pm(files) }),
4300
+ runtime_check: xucode_preview.runtime_check({ project_id, manifest, package_manager: detect_pm(files) }),
4129
4301
  };
4130
4302
  } catch (err) {
4303
+ console.warn(`[xucode] could not read the project on its machine: ${err?.message || err}`);
4131
4304
  return { manifest: null, files: [], node_modules_present: false };
4132
4305
  }
4133
4306
  };
@@ -11339,8 +11512,8 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11339
11512
  const from_code_surface = get_dashboard_context_obj(req, conversation_doc)?.code_surface === true;
11340
11513
  const project_repo = from_code_surface ? (await git_repo_list({ uid, profile_id, app_id: target_app_id })).data?.repos?.[0] : null;
11341
11514
 
11342
- // ── UI-228: the Xudex route ─────────────────────────────────────────────────────────
11343
- // The same surface, a different engine underneath. A Xudex run adds what the Code tab
11515
+ // ── UI-228: the Xucode route ─────────────────────────────────────────────────────────
11516
+ // The same surface, a different engine underneath. A Xucode run adds what the Code tab
11344
11517
  // cannot do today: it verifies the change against the project's own build and tests before
11345
11518
  // answering, hands failures back for a bounded repair, and records what the run cost.
11346
11519
  //
@@ -11348,9 +11521,9 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11348
11521
  // path below is dev-verified and working; this one is newer. `route_code_tab` is false in
11349
11522
  // prod, true on dev, so it can be turned on for real once it has been watched, and turned
11350
11523
  // off in one config edit if it misbehaves, with no deploy and no code change.
11351
- if (project_repo && _conf.xudex?.enabled === true && _conf.xudex?.route_code_tab === true) {
11352
- emitToDashboard('stream_phase', 'Starting Xudex run');
11353
- const xret = await xudex_run({
11524
+ if (project_repo && _conf.xucode?.enabled === true && _conf.xucode?.route_code_tab === true) {
11525
+ emitToDashboard('stream_phase', 'Starting Xucode run');
11526
+ const xret = await xucode_run({
11354
11527
  uid,
11355
11528
  profile_id,
11356
11529
  app_id: target_app_id,
@@ -11360,7 +11533,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11360
11533
  // workspace already puts code_surface and project_id, so read it from there first and
11361
11534
  // fall back to a top-level field for any caller that sets one. Unset means the engine
11362
11535
  // registry's own default decides, rather than a name hard-coded here.
11363
- engine: get_dashboard_context_obj(req, conversation_doc)?.xudex_engine || req.xudex_engine || null,
11536
+ engine: get_dashboard_context_obj(req, conversation_doc)?.xucode_engine || req.xucode_engine || null,
11364
11537
  model: req.codex_model || req.ai_model || null,
11365
11538
  resume_session_id: conversation_doc.codex_session_id && conversation_doc.codex_session_host === code_run_host() ? conversation_doc.codex_session_id : null,
11366
11539
  on_event: (event) => {
@@ -11403,7 +11576,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11403
11576
  emitToDashboard('stream_end');
11404
11577
  // A code_run doc, so the workspace panel keeps working unchanged. The panel loads the
11405
11578
  // latest run for a conversation and renders files, diffs, revert and the git header from
11406
- // it; without this a xudex run would leave it saying "nothing has run in this chat yet"
11579
+ // it; without this a xucode run would leave it saying "nothing has run in this chat yet"
11407
11580
  // right after a run, and every one of those features would have to be rebuilt against a
11408
11581
  // second representation of the same thing.
11409
11582
  try {
@@ -11411,7 +11584,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11411
11584
  _id: await _common.xuda_get_uuid('code_run'),
11412
11585
  docType: 'code_run',
11413
11586
  stat: 3,
11414
- engine: xret.data.engine || 'xudex',
11587
+ engine: xret.data.engine || 'xucode',
11415
11588
  state: xret.data.ok ? 'done' : 'failed',
11416
11589
  uid,
11417
11590
  profile_id,
@@ -11427,7 +11600,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11427
11600
  started_ts: xret.record?.started_ts || Date.now(),
11428
11601
  ended_ts: xret.record?.ended_ts || Date.now(),
11429
11602
  usage: xret.record?.tokens || null,
11430
- // Revert is deliberately NOT offered on a xudex run yet: the file lives on the
11603
+ // Revert is deliberately NOT offered on a xucode run yet: the file lives on the
11431
11604
  // machine, and code_run_revert_file works against the region server's own copy. A
11432
11605
  // Revert button that quietly did nothing is worse than no button.
11433
11606
  files: (xret.data.files || []).map((f) => ({ path: f.rel, rel: f.rel, change: f.status === '??' ? 'add' : f.status === 'D' ? 'delete' : 'change', diff_available: false, revert_available: false })),
@@ -11443,10 +11616,10 @@ ${conversation_history || `User (dashboard): ${prompt}`}
11443
11616
  ts: Date.now(),
11444
11617
  });
11445
11618
  } catch (e) {
11446
- console.error(`[xudex] could not save the run doc: ${e.message}`);
11619
+ console.error(`[xucode] could not save the run doc: ${e.message}`);
11447
11620
  }
11448
11621
 
11449
- return await saveAssistantItem(answer, { xudex: true, verified: xret.data.ok, files: xret.data.files, branch: xret.data.branch });
11622
+ return await saveAssistantItem(answer, { xucode: true, verified: xret.data.ok, files: xret.data.files, branch: xret.data.branch });
11450
11623
  }
11451
11624
 
11452
11625
  if (project_repo) {