@melaya/runner 1.0.98 → 1.0.100
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/dist/assistantHost.py +25 -2
- package/dist/connection.js +18 -9
- package/dist/modelLoader.js +21 -0
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -72,6 +72,28 @@ def _memory_watermark(agent) -> int:
|
|
|
72
72
|
return 0
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def _config_hash() -> str:
|
|
76
|
+
"""PR4 config-drift: a stable hash over the config env that determines host
|
|
77
|
+
behaviour, canonicalized IDENTICALLY to the server (runnerNamespace.ts
|
|
78
|
+
_assistantConfigHash): pipe-delimited provider|model|language|connectors|
|
|
79
|
+
hitl|phoneReady, language default 'en', connectors lowercased+sorted,
|
|
80
|
+
phoneReady 1/0. Reported in `ready` so the server can detect env divergence."""
|
|
81
|
+
import hashlib
|
|
82
|
+
raw_conn = os.environ.get("MEL_ASSISTANT_CONNECTORS", "") or ""
|
|
83
|
+
connectors = ",".join(sorted(c.lower() for c in raw_conn.split(",") if c.strip()))
|
|
84
|
+
hitl = os.environ.get("MEL_ASSISTANT_HITL_MODE", "safe") or "safe"
|
|
85
|
+
if hitl not in ("safe", "autonomous", "payments_only"):
|
|
86
|
+
hitl = "safe"
|
|
87
|
+
phone = "1" if (os.environ.get("MEL_ASSISTANT_PHONE_READY", "") or "") else "0"
|
|
88
|
+
canon = "|".join([
|
|
89
|
+
os.environ.get("MEL_ASSISTANT_PROVIDER", "") or "",
|
|
90
|
+
os.environ.get("MEL_ASSISTANT_MODEL", "") or "",
|
|
91
|
+
os.environ.get("MEL_ASSISTANT_LANGUAGE", "en") or "en",
|
|
92
|
+
connectors, hitl, phone,
|
|
93
|
+
])
|
|
94
|
+
return hashlib.sha256(canon.encode("utf-8")).hexdigest()[:32]
|
|
95
|
+
|
|
96
|
+
|
|
75
97
|
def _render_summary_text(summary) -> str:
|
|
76
98
|
"""Render a server AssistantSummary ({facts, provenance}) to a data-only text
|
|
77
99
|
block for the compressed-summary slot. Never policy — reference only."""
|
|
@@ -561,7 +583,7 @@ def main() -> int:
|
|
|
561
583
|
except Exception:
|
|
562
584
|
_log("compaction hooks unavailable (non-fatal)")
|
|
563
585
|
|
|
564
|
-
_emit("", "ready", memoryWatermark=_memory_watermark(agent), hostInstanceId=_HOST_ID, generation=_GENERATION)
|
|
586
|
+
_emit("", "ready", memoryWatermark=_memory_watermark(agent), hostInstanceId=_HOST_ID, generation=_GENERATION, configHash=_config_hash())
|
|
565
587
|
_log("ready")
|
|
566
588
|
|
|
567
589
|
q: "queue.Queue[str]" = queue.Queue()
|
|
@@ -592,7 +614,8 @@ def main() -> int:
|
|
|
592
614
|
gen = int(req.get("generation") or 0)
|
|
593
615
|
except Exception:
|
|
594
616
|
gen = 0
|
|
595
|
-
|
|
617
|
+
# Strict: gen 0 is a real generation (pre-migration), not a wildcard.
|
|
618
|
+
if gen != _GENERATION:
|
|
596
619
|
_log(f"restore for stale generation {gen} (mine={_GENERATION}) — ignored")
|
|
597
620
|
continue
|
|
598
621
|
mem = _agent_memory(agent)
|
package/dist/connection.js
CHANGED
|
@@ -885,10 +885,12 @@ export async function connect(opts) {
|
|
|
885
885
|
return;
|
|
886
886
|
const generation = Number(payload.generation ?? 0) || 0;
|
|
887
887
|
const emitEv = (ev) => socket.emit("runner:assistant_event", { sessionId: sid, turnId: "", ...ev });
|
|
888
|
-
// A live host already exists — report ready
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
|
|
888
|
+
// A live host already exists — report ready WITH its last-known memory
|
|
889
|
+
// watermark so the server doesn't needlessly re-offer a rehydrate (a bare
|
|
890
|
+
// ready would read as watermark 0). No second spawn.
|
|
891
|
+
const live = activeAssistants.get(sid);
|
|
892
|
+
if (live) {
|
|
893
|
+
emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0 });
|
|
892
894
|
return;
|
|
893
895
|
}
|
|
894
896
|
// A boot is already in flight for this sid (concurrent start / server restart
|
|
@@ -987,7 +989,7 @@ export async function connect(opts) {
|
|
|
987
989
|
...(payload.credentials || {}),
|
|
988
990
|
};
|
|
989
991
|
const proc = spawn(envResult.pythonPath, ["-u", stagedHost], { cwd: workDir, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
990
|
-
const session = { proc, lastActivity: Date.now(), stdoutBuf: "", generation };
|
|
992
|
+
const session = { proc, lastActivity: Date.now(), stdoutBuf: "", generation, memoryWatermark: 0 };
|
|
991
993
|
activeAssistants.set(sid, session);
|
|
992
994
|
console.log(chalk.hex("#7c6ff0")(` ◆ Assistant session ${sid.slice(0, 10)}… (${payload.provider})`));
|
|
993
995
|
proc.stdout?.on("data", (data) => {
|
|
@@ -999,7 +1001,13 @@ export async function connect(opts) {
|
|
|
999
1001
|
const t = line.trim();
|
|
1000
1002
|
if (t.startsWith("MELASSIST ")) {
|
|
1001
1003
|
try {
|
|
1002
|
-
|
|
1004
|
+
const parsed = JSON.parse(t.slice("MELASSIST ".length));
|
|
1005
|
+
// Track the host's latest memory watermark (ready / rehydrated
|
|
1006
|
+
// frames report it) so a re-`start` of the live host reports a
|
|
1007
|
+
// truthful watermark and the server skips a needless rehydrate.
|
|
1008
|
+
if (typeof parsed?.memoryWatermark === "number")
|
|
1009
|
+
session.memoryWatermark = parsed.memoryWatermark;
|
|
1010
|
+
socket.emit("runner:assistant_event", { sessionId: sid, ...parsed });
|
|
1003
1011
|
}
|
|
1004
1012
|
catch { /* skip malformed */ }
|
|
1005
1013
|
}
|
|
@@ -1046,10 +1054,11 @@ export async function connect(opts) {
|
|
|
1046
1054
|
const s = activeAssistants.get(sid);
|
|
1047
1055
|
if (!s)
|
|
1048
1056
|
return;
|
|
1049
|
-
// Bind to the boot generation — a frame
|
|
1050
|
-
// live host is dropped (the host also re-checks).
|
|
1057
|
+
// Bind to the boot generation — a frame whose generation differs from the
|
|
1058
|
+
// live host's is dropped (the host also re-checks). Strict equality: gen 0 is
|
|
1059
|
+
// a real generation (pre-migration conversations), not a wildcard.
|
|
1051
1060
|
const gen = Number(payload.generation ?? 0) || 0;
|
|
1052
|
-
if (gen
|
|
1061
|
+
if (gen !== s.generation)
|
|
1053
1062
|
return;
|
|
1054
1063
|
s.lastActivity = Date.now();
|
|
1055
1064
|
try {
|
package/dist/modelLoader.js
CHANGED
|
@@ -378,6 +378,27 @@ export async function preflightOllama(modelName, onProgress = () => { }) {
|
|
|
378
378
|
};
|
|
379
379
|
}
|
|
380
380
|
const profile = classifyModel(modelName);
|
|
381
|
+
// PR5: read the REAL trained context via /api/show and expose it as
|
|
382
|
+
// contextTokens (clamped to the operator cap, mirroring the Python
|
|
383
|
+
// detect_ollama_context). connection.ts forwards this as
|
|
384
|
+
// MEL_MODEL_CONTEXT_TOKENS so the TS profile matches the Python-detected
|
|
385
|
+
// window (the Python side self-detects too, so this is belt-and-suspenders).
|
|
386
|
+
try {
|
|
387
|
+
const cap = Number(process.env.MEL_OLLAMA_MAX_CTX || 16384) || 16384;
|
|
388
|
+
const show = await fetchWithTimeout(`${OLLAMA_BASE}/api/show`, {
|
|
389
|
+
timeoutMs: 3_000, method: "POST", body: JSON.stringify({ model: modelName }),
|
|
390
|
+
headers: { "content-type": "application/json" },
|
|
391
|
+
});
|
|
392
|
+
if (show.ok) {
|
|
393
|
+
const sj = await show.json();
|
|
394
|
+
const info = sj.model_info || {};
|
|
395
|
+
const key = Object.keys(info).find((k) => k.endsWith(".context_length"));
|
|
396
|
+
const trained = key ? Number(info[key]) : 0;
|
|
397
|
+
if (trained > 0)
|
|
398
|
+
profile.contextTokens = Math.max(2048, Math.min(trained, cap));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch { /* best-effort — Python self-detects if this is missing */ }
|
|
381
402
|
if (profile.tier !== "agentic-capable") {
|
|
382
403
|
onProgress(`⚠ ${modelName} classified ${profile.tier} (${profile.reason})`);
|
|
383
404
|
}
|