@melaya/runner 1.0.102 → 1.0.104

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.
@@ -271,28 +271,29 @@ def _build_agent():
271
271
  # then ALWAYS require the in-chat approval card (fail-safe).
272
272
  if connector_services and not os.environ.get("MEL_ASSISTANT_CONNECTOR_HITL"):
273
273
  os.environ["MEL_ASSISTANT_CONNECTOR_HITL"] = "1"
274
+ # Core primitives (melaya_core): web_search / web_fetch, files, HTTP, data +
275
+ # office utilities, scraping, encoding, SQL, etc. — are ALWAYS discoverable via
276
+ # search_tools/activate_tool. The side-effecting ones run on the USER's OWN
277
+ # machine (this runner) and are HITL-gated by the assistant's safe/payments
278
+ # modes, so exposing them for discovery is safe. They stay in the LAZY pool
279
+ # (not pinned) so the ~73-tool category never explodes the active budget.
280
+ core_categories = ["melaya_core"]
274
281
  try:
275
- if connector_services:
276
- from shared.orchestration.lazy_registry import build_lazy_toolkit
277
- # Phone tools must ALWAYS be fully passed, never scoped: a phone task
278
- # can't afford search_tools/activate_tool round-trips per tap (that is
279
- # the "why did it restrict the tools + it's slow" regression). The lazy
280
- # toolkit pins active_categories only UP TO `budget` (default 25), so
281
- # phone (~23) + melaya_agent got bumped into the deferred pool. When
282
- # phone is enabled, widen the budget so the WHOLE phone + base set stays
283
- # active/pinned; the connectors still lazy-defer beyond that.
284
- _budget = int(os.environ.get("MEL_LAZY_BUDGET", "25"))
285
- if phone_enabled:
286
- _budget = max(_budget, 64)
287
- toolkit = build_lazy_toolkit(
288
- active_categories=categories,
289
- include_categories=categories + connector_services,
290
- budget=_budget,
291
- )
292
- else:
293
- toolkit = build_toolkit(categories=categories)
282
+ from shared.orchestration.lazy_registry import build_lazy_toolkit
283
+ # Base tools (melaya_agent + phone) stay active+pinned; core + any selected
284
+ # connectors are deferred + discoverable. Phone tasks need the WHOLE phone
285
+ # set pinned (no per-tap search_tools round-trips the old "slow" regression),
286
+ # so widen the budget when phone is enabled; core/connectors still lazy-defer.
287
+ _budget = int(os.environ.get("MEL_LAZY_BUDGET", "25"))
288
+ if phone_enabled:
289
+ _budget = max(_budget, 64)
290
+ toolkit = build_lazy_toolkit(
291
+ active_categories=categories,
292
+ include_categories=categories + core_categories + connector_services,
293
+ budget=_budget,
294
+ )
294
295
  except Exception as exc:
295
- _log(f"toolkit build failed (connectors={connector_services}): {exc}; retrying melaya_agent only")
296
+ _log(f"toolkit build failed (connectors={connector_services}, core): {exc}; retrying melaya_agent only")
296
297
  try:
297
298
  toolkit = build_toolkit(categories=["melaya_agent"])
298
299
  except Exception:
@@ -14,7 +14,7 @@ import { io } from "socket.io-client";
14
14
  import chalk from "chalk";
15
15
  import ora from "ora";
16
16
  import { spawn } from "child_process";
17
- import { writeFileSync, mkdirSync, existsSync } from "fs";
17
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
18
18
  import { dirname, join } from "path";
19
19
  import { fileURLToPath } from "url";
20
20
  import { tmpdir, homedir } from "os";
@@ -28,6 +28,19 @@ import { ensureSharedModules, getSharedDir } from "./sharedVendor.js";
28
28
  import { startLumaBrowserBridge } from "./lumaBrowserBridge.js";
29
29
  const HEARTBEAT_INTERVAL = 30_000;
30
30
  const activeProcesses = new Map();
31
+ // PR4/P1-5: the assistant-session wire protocol version. Bumped whenever the
32
+ // handshake contract changes (this rev adds hostInstanceId + configHash + a
33
+ // versioned ready/hello). The server enforces a MINIMUM: a runner advertising a
34
+ // lower protocol never gets an assistant session routed to it (it gets a clear
35
+ // "update your runner" instead of a silent stale-memory turn).
36
+ const ASSISTANT_PROTOCOL_VERSION = 2;
37
+ // Best-effort read of our own package version (mirrors cli.ts). Advertised in the
38
+ // hello so the server can log / gate on a package floor too.
39
+ let RUNNER_VERSION = "0.0.0";
40
+ try {
41
+ RUNNER_VERSION = String(JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version || "0.0.0");
42
+ }
43
+ catch { /* dev tree layout differs — non-fatal */ }
31
44
  const activeAssistants = new Map();
32
45
  // PR4: sid-keyed boot reservation. The guard-then-spawn in runner:assistant_start
33
46
  // has an async gap (ensureSharedModules / ensurePythonEnv awaits) between the
@@ -133,7 +146,7 @@ export async function connect(opts) {
133
146
  // Advertise capabilities so the server only routes assistant chat sessions
134
147
  // to runners new enough to host them (older runners never get one → the
135
148
  // server falls back with a clear "update your runner" instead of hanging).
136
- socket.emit("runner:hello", { capabilities: ["assistant_session"], package: "@melaya/runner" });
149
+ socket.emit("runner:hello", { capabilities: ["assistant_session"], package: "@melaya/runner", version: RUNNER_VERSION, assistantProtocol: ASSISTANT_PROTOCOL_VERSION });
137
150
  // Start local event relay
138
151
  if (!relay) {
139
152
  relay = await startLocalRelay(socket, opts.verbose, opts.serverUrl);
@@ -894,7 +907,7 @@ export async function connect(opts) {
894
907
  // that booted under an older generation and reboot it instead of serving the
895
908
  // next turn from stale memory/config. A bare ready hid this.
896
909
  if (live) {
897
- emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0, generation: live.generation });
910
+ emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0, generation: live.generation, hostInstanceId: live.hostInstanceId, configHash: live.configHash, protocol: ASSISTANT_PROTOCOL_VERSION });
898
911
  return;
899
912
  }
900
913
  // A boot is already in flight for this sid (concurrent start / server restart
@@ -993,7 +1006,10 @@ export async function connect(opts) {
993
1006
  ...(payload.credentials || {}),
994
1007
  };
995
1008
  const proc = spawn(envResult.pythonPath, ["-u", stagedHost], { cwd: workDir, env, stdio: ["pipe", "pipe", "pipe"] });
996
- const session = { proc, lastActivity: Date.now(), stdoutBuf: "", generation, memoryWatermark: 0 };
1009
+ // hostInstanceId + configHash are filled from the host's OWN `ready` frame
1010
+ // (assistantHost.py _HOST_ID / _config_hash) as it boots — authoritative and
1011
+ // server-aligned. Start empty; the stdout parser captures them.
1012
+ const session = { proc, lastActivity: Date.now(), stdoutBuf: "", generation, memoryWatermark: 0, hostInstanceId: "", configHash: "" };
997
1013
  activeAssistants.set(sid, session);
998
1014
  console.log(chalk.hex("#7c6ff0")(` ◆ Assistant session ${sid.slice(0, 10)}… (${payload.provider})`));
999
1015
  proc.stdout?.on("data", (data) => {
@@ -1011,6 +1027,16 @@ export async function connect(opts) {
1011
1027
  // truthful watermark and the server skips a needless rehydrate.
1012
1028
  if (typeof parsed?.memoryWatermark === "number")
1013
1029
  session.memoryWatermark = parsed.memoryWatermark;
1030
+ // CAPTURE the host's authoritative identity off its `ready` frame
1031
+ // (_HOST_ID / _config_hash) so the live-host ready path can re-report
1032
+ // them without the host re-emitting; ADD only the wire protocol.
1033
+ if (parsed?.kind === "ready") {
1034
+ if (typeof parsed.hostInstanceId === "string")
1035
+ session.hostInstanceId = parsed.hostInstanceId;
1036
+ if (typeof parsed.configHash === "string")
1037
+ session.configHash = parsed.configHash;
1038
+ parsed.protocol = ASSISTANT_PROTOCOL_VERSION;
1039
+ }
1014
1040
  socket.emit("runner:assistant_event", { sessionId: sid, ...parsed });
1015
1041
  }
1016
1042
  catch { /* skip malformed */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,