@melaya/runner 1.0.95 → 1.0.97

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.
@@ -177,10 +177,15 @@ def _build_agent():
177
177
  model = os.environ.get("MEL_ASSISTANT_MODEL", "") or None
178
178
  language = os.environ.get("MEL_ASSISTANT_LANGUAGE", "en")
179
179
 
180
- # Phone control is offered ONLY on the native mobile app surface the same
181
- # gate as the server-side melaya_run_phone_task tool. A desktop / web chat
182
- # never gets the phone_* tools, even if a phone is paired.
183
- phone_enabled = os.environ.get("MEL_ASSISTANT_SURFACE", "") == "mobile-native"
180
+ # Phone control unlocks on the native mobile app surface OR whenever the user
181
+ # has a PAIRED phone (MEL_ASSISTANT_PHONE_READY, set by the server from
182
+ # getPhonePresence). A desktop / web chat drives the paired phone REMOTELY
183
+ # through the same Redis queue, so it must not be surface-gated — without this,
184
+ # desktop chats had zero phone_* tools and the model refused phone tasks.
185
+ phone_enabled = (
186
+ os.environ.get("MEL_ASSISTANT_SURFACE", "") == "mobile-native"
187
+ or os.environ.get("MEL_ASSISTANT_PHONE_READY", "") in ("1", "true", "True")
188
+ )
184
189
 
185
190
  # Read-only platform tools (melaya_agent); phone control (Device Control) on
186
191
  # mobile only. These POST to /api/v1/private/assistant-tool + /phone/command
@@ -201,10 +206,20 @@ def _build_agent():
201
206
  try:
202
207
  if connector_services:
203
208
  from shared.orchestration.lazy_registry import build_lazy_toolkit
209
+ # Phone tools must ALWAYS be fully passed, never scoped: a phone task
210
+ # can't afford search_tools/activate_tool round-trips per tap (that is
211
+ # the "why did it restrict the tools + it's slow" regression). The lazy
212
+ # toolkit pins active_categories only UP TO `budget` (default 25), so
213
+ # phone (~23) + melaya_agent got bumped into the deferred pool. When
214
+ # phone is enabled, widen the budget so the WHOLE phone + base set stays
215
+ # active/pinned; the connectors still lazy-defer beyond that.
216
+ _budget = int(os.environ.get("MEL_LAZY_BUDGET", "25"))
217
+ if phone_enabled:
218
+ _budget = max(_budget, 64)
204
219
  toolkit = build_lazy_toolkit(
205
220
  active_categories=categories,
206
221
  include_categories=categories + connector_services,
207
- budget=int(os.environ.get("MEL_LAZY_BUDGET", "25")),
222
+ budget=_budget,
208
223
  )
209
224
  else:
210
225
  toolkit = build_toolkit(categories=categories)
@@ -237,7 +252,10 @@ def _build_agent():
237
252
  "phone_ask_user(question): it expands the Melaya tile ON THE PHONE into a reply "
238
253
  "card and BLOCKS for the user's typed answer WITHOUT ending your turn, so the run "
239
254
  "continues seamlessly the moment they reply. Use it SPARINGLY, for real decisions "
240
- "only — not routine ones. The EXACT-APP rule still holds (a missing app is a stop, "
255
+ "only — not routine ones. Do NOT call phone_ask_user to cope with repeated tool "
256
+ "ERRORS or because you feel stuck/struggling — that is a failure to REPORT, not a "
257
+ "decision for the user; stop and report what failed instead. Only ask for a genuine "
258
+ "who/which fork. The EXACT-APP rule still holds (a missing app is a stop, "
241
259
  "not a substitution). If a feed is algorithmic, use the app's own Search / Explore "
242
260
  "to find the right content yourself. Keep going until the task is COMPLETE (e.g. "
243
261
  "all N comments posted) or you are genuinely blocked; only then report what you "
@@ -77,11 +77,42 @@ export async function connect(opts) {
77
77
  const localVersion = getLocalSharedVersion();
78
78
  if (!localVersion)
79
79
  return; // nothing cached yet — let assistant_start do it
80
- const { ensurePythonEnv } = await import("./pythonEnv.js");
81
- await ensurePythonEnv(opts.pythonPath, localVersion, (m) => { if (opts.verbose)
80
+ const { ensurePythonEnv, getCertBundlePath } = await import("./pythonEnv.js");
81
+ const envResult = await ensurePythonEnv(opts.pythonPath, localVersion, (m) => { if (opts.verbose)
82
82
  console.log(chalk.gray(` [assistant prewarm] ${m}`)); });
83
83
  if (opts.verbose)
84
84
  console.log(chalk.gray(" [assistant prewarm] ready"));
85
+ // ALSO pre-import the heavy Python stack so the first real run/chat
86
+ // doesn't pay the ~6s cold `import shared.runtime.registry`. Spawn it
87
+ // detached + fire-and-forget with the SAME PYTHONPATH/env a real
88
+ // pipeline spawn uses (see the runner:run handler below) so `shared.*`
89
+ // resolves; this heats the OS file cache + bytecode. Never blocks
90
+ // connect, never throws — swallow everything. Only when a shared
91
+ // bundle exists locally (mirror the ensurePythonEnv guard above).
92
+ if (envResult.ok) {
93
+ try {
94
+ const sharedDir = getSharedDir();
95
+ if (existsSync(sharedDir)) {
96
+ const certBundle = getCertBundlePath();
97
+ const warmEnv = {
98
+ ...process.env,
99
+ PYTHONPATH: sharedDir,
100
+ PYTHONIOENCODING: "utf-8",
101
+ PYTHONUTF8: "1",
102
+ ...(certBundle ? { SSL_CERT_FILE: certBundle, REQUESTS_CA_BUNDLE: certBundle } : {}),
103
+ };
104
+ const warmChild = spawn(envResult.pythonPath, ["-c", "import shared.runtime.registry"], { env: warmEnv, stdio: "ignore" });
105
+ warmChild.on("error", () => { });
106
+ warmChild.on("exit", (code) => {
107
+ if (code === 0 && opts.verbose) {
108
+ console.log(chalk.gray(" [assistant prewarm] import-warmed"));
109
+ }
110
+ });
111
+ warmChild.unref();
112
+ }
113
+ }
114
+ catch { /* fire-and-forget — never throw */ }
115
+ }
85
116
  }
86
117
  catch (e) {
87
118
  if (opts.verbose)
@@ -901,6 +932,10 @@ export async function connect(opts) {
901
932
  MEL_ASSISTANT_MODEL: String(payload.model || ""),
902
933
  MEL_ASSISTANT_LANGUAGE: String(payload.language || "en"),
903
934
  MEL_ASSISTANT_SURFACE: String(payload.surface || ""),
935
+ // Phone toolkit unlocks when the user has a paired phone, on ANY surface
936
+ // (desktop drives the paired phone remotely). The host ORs this with the
937
+ // mobile-native surface check. Empty ⇒ not paired ⇒ no phone tools.
938
+ MEL_ASSISTANT_PHONE_READY: payload.phoneReady ? "1" : "",
904
939
  // Connector tool sets the user enabled for this chat (comma-joined service
905
940
  // ids). The host seeds a lazy toolkit from these so ANY connector's tools
906
941
  // are reachable without exploding context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.95",
3
+ "version": "1.0.97",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,