meshcode 2.11.115__tar.gz → 2.11.117__tar.gz
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.
- {meshcode-2.11.115 → meshcode-2.11.117}/PKG-INFO +1 -1
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/__init__.py +1 -1
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/_session_handoff_template.py +49 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/comms_v4.py +1 -1
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/server.py +25 -240
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/run_agent.py +52 -5
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/self_update.py +148 -2
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.11.115 → meshcode-2.11.117}/pyproject.toml +1 -1
- {meshcode-2.11.115 → meshcode-2.11.117}/README.md +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/__main__.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/_stop_hook_template.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/ascii_art.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/atomic_push.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/claude_update.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/cli.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/compat.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/daemon.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/date_parse.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/doctor.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/error_hints.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/exceptions.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/hooks/__init__.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/hooks/repo_path_lock.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/hostd.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/invites.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/launcher.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/launcher_install.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/backend.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/realtime.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/preferences.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/protocol_handler.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/quickstart.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/rpc_allowlist.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/scripts/check_secrets.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/scripts/race_rate_harness.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/secrets.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/setup_clients.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/supervisor.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/up.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode/upload.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/setup.cfg +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_auto_update_hardening.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_autonomous_closegap_1.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_autonomous_closegap_2.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_autonomous_closegap_3.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_autonomous_prompt_inject.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_boot_bug_regression.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_color_truecolor.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_core.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_cross_agent_messaging.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_date_parse.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_doctor.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_epistemic_v1_python_sdk.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_epistemic_v1_stop_conditions.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_esc_deaf_state.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_exceptions.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_file_upload.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_init_device_code.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_install_guard.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_lease_sigterm_release.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_mark_read_batch.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_marketplace_ratings.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_migration_integrity.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_realtime_event_freshness.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_rls_cross_tenant.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_rpc_grants.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_rpc_migrations.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_run_agent_dry_run.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_run_agent_no_server_import.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_security_regressions.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_self_update_user_site.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_sentinel.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_setup_path.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_sleep_signals.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_status_enum_coverage.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_stay_on_loop_hook.py +0 -0
- {meshcode-2.11.115 → meshcode-2.11.117}/tests/test_wait_open_tasks_contradiction.py +0 -0
|
@@ -161,6 +161,53 @@ def _request_recycle_if_marked(project_dir) -> None:
|
|
|
161
161
|
sys.stderr.write(f"[session_handoff_write] recycle-request skipped: {e}\\n")
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def _persist_handoff_to_memory(project_dir, handoff) -> None:
|
|
165
|
+
"""L6 M6.1 (task 84c426d4, 2.11.114): mirror the handoff into
|
|
166
|
+
mc_agent_memory key='session_handoff' so the server-side boot
|
|
167
|
+
continuity_capsule (mig 456) can surface it on the NEXT session even
|
|
168
|
+
when the local handoff.json is gone (new host, wiped workspace).
|
|
169
|
+
Same best-effort creds pattern as _request_recycle_if_marked —
|
|
170
|
+
any failure silently skips; handoff.json already covers the local path.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
mcp = json.loads((project_dir / ".mcp.json").read_text(encoding="utf-8"))
|
|
174
|
+
env = (next(iter((mcp.get("mcpServers") or {}).values()), {}) or {}).get("env", {}) or {}
|
|
175
|
+
url = env.get("SUPABASE_URL"); key = env.get("SUPABASE_KEY")
|
|
176
|
+
agent = env.get("MESHCODE_AGENT"); project = env.get("MESHCODE_PROJECT")
|
|
177
|
+
if not (url and key and agent):
|
|
178
|
+
return
|
|
179
|
+
api_key = os.environ.get("MESHCODE_API_KEY")
|
|
180
|
+
if not api_key:
|
|
181
|
+
try:
|
|
182
|
+
import importlib
|
|
183
|
+
api_key = importlib.import_module("meshcode.secrets").get_api_key(
|
|
184
|
+
profile=env.get("MESHCODE_KEYCHAIN_PROFILE") or "default")
|
|
185
|
+
except Exception:
|
|
186
|
+
api_key = None
|
|
187
|
+
if not api_key:
|
|
188
|
+
return
|
|
189
|
+
turns = handoff.get("turns") or []
|
|
190
|
+
compact = {
|
|
191
|
+
"trigger": handoff.get("trigger"),
|
|
192
|
+
"captured_at_session": handoff.get("session_id"),
|
|
193
|
+
"tail": [{"role": t["role"], "text": t["text"][:400]} for t in turns[-8:]],
|
|
194
|
+
}
|
|
195
|
+
import urllib.request as _u
|
|
196
|
+
body = json.dumps({
|
|
197
|
+
"p_api_key": api_key, "p_agent_name": agent,
|
|
198
|
+
"p_key": "session_handoff", "p_value": compact,
|
|
199
|
+
"p_tier": "episodic", "p_project_name": project,
|
|
200
|
+
}).encode("utf-8")
|
|
201
|
+
req = _u.Request(
|
|
202
|
+
url.rstrip("/") + "/rest/v1/rpc/mc_memory_set",
|
|
203
|
+
data=body, method="POST",
|
|
204
|
+
headers={"apikey": key, "Authorization": "Bearer " + key,
|
|
205
|
+
"Content-Type": "application/json"})
|
|
206
|
+
_u.urlopen(req, timeout=5).read()
|
|
207
|
+
except Exception as e: # noqa: BLE001 — never block compaction
|
|
208
|
+
sys.stderr.write(f"[session_handoff_write] memory-persist skipped: {e}\\n")
|
|
209
|
+
|
|
210
|
+
|
|
164
211
|
def main() -> int:
|
|
165
212
|
try:
|
|
166
213
|
raw = sys.stdin.read()
|
|
@@ -185,6 +232,8 @@ def main() -> int:
|
|
|
185
232
|
tmp.replace(d / "handoff.json")
|
|
186
233
|
except OSError as e:
|
|
187
234
|
sys.stderr.write(f"[session_handoff_write] skipped: {e}\\n")
|
|
235
|
+
# L6 M6.1: mirror to server-side memory for the boot continuity capsule.
|
|
236
|
+
_persist_handoff_to_memory(_project_dir(), handoff)
|
|
188
237
|
# CTX-CLOSE-RELAUNCH (task 400fc536): now that the thread is snapshotted,
|
|
189
238
|
# commander-tier sessions ask the server to recycle at the next task-edge.
|
|
190
239
|
_request_recycle_if_marked(_project_dir())
|
|
@@ -1886,7 +1886,7 @@ def _start_heartbeat_daemon(project, name, agent_pid=None):
|
|
|
1886
1886
|
" if not check_still_leased(pid):\n"
|
|
1887
1887
|
" sys.exit(0)\n"
|
|
1888
1888
|
" post('/rest/v1/rpc/mc_heartbeat', {'p_project_id':pid,'p_agent_name':name})\n"
|
|
1889
|
-
" time.sleep(30)\n"
|
|
1889
|
+
" time.sleep(30)\n" # .117: back to 30s (commander 2026-06-09 — 10s tripled hb RPC volume; Fix A already bounds the stale window)
|
|
1890
1890
|
)
|
|
1891
1891
|
# Windows: start_new_session kwarg doesn't exist. Use creationflags.
|
|
1892
1892
|
_popen_kwargs = {
|
|
@@ -1160,6 +1160,11 @@ def _is_consequential(tool_name: str) -> bool:
|
|
|
1160
1160
|
|
|
1161
1161
|
|
|
1162
1162
|
def _check_decision_gate(tool_name: str) -> Optional[Dict[str, Any]]:
|
|
1163
|
+
# R2-4 kill-list (.116): epistemic suite removed (meshcode_decide is gone) —
|
|
1164
|
+
# the gate would otherwise block consequential tools demanding a tool that
|
|
1165
|
+
# no longer exists. Permanently open.
|
|
1166
|
+
return None
|
|
1167
|
+
|
|
1163
1168
|
"""Returns None when the gate is open; returns an error dict to be
|
|
1164
1169
|
surfaced as the tool result when the gate is closed. Soft-fail on RPC
|
|
1165
1170
|
error (gate opens) so a transient DB blip doesn't trap the agent."""
|
|
@@ -1842,7 +1847,7 @@ Then: set_status(online,ready) → check() → tasks() → auto_wake() → statu
|
|
|
1842
1847
|
|
|
1843
1848
|
COMMUNICATE BY CALLING TOOLS, not by thinking aloud. Cross-mesh: send(to="agent@meshwork"). Reference docs (memory/scratchpad/account ops) → recall agent_protocol_quick_ref when needed.
|
|
1844
1849
|
|
|
1845
|
-
|
|
1850
|
+
Memory hints with open_contradictions take a -0.2 ranking penalty — treat as a flag, not a veto.
|
|
1846
1851
|
|
|
1847
1852
|
ESCALATION RULES:
|
|
1848
1853
|
- MUST escalate to human: destructive ops (drop/delete/rm -rf), irreversible changes (force-push main, mig without rollback), product-direction decisions (what feature next), legal/compliance, security (auth/secrets/PII), spend > $50 USD.
|
|
@@ -4316,13 +4321,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
4316
4321
|
# surface autonomy on the refused-path too so check
|
|
4317
4322
|
# parity holds when wait short-circuits on unread msgs.
|
|
4318
4323
|
resp["you_are_autonomous"] = _get_autonomous_mode()
|
|
4319
|
-
#
|
|
4320
|
-
try:
|
|
4321
|
-
_pa = _promise_awareness(_get_api_key())
|
|
4322
|
-
if _pa:
|
|
4323
|
-
resp["time_awareness"] = _pa
|
|
4324
|
-
except Exception:
|
|
4325
|
-
pass
|
|
4324
|
+
# time_awareness removed with promise infra (R2-4 kill-list, .116)
|
|
4326
4325
|
return resp
|
|
4327
4326
|
# Ack-only batch — fall through to wait loop
|
|
4328
4327
|
except Exception:
|
|
@@ -6141,15 +6140,6 @@ def meshcode_boot() -> Dict[str, Any]:
|
|
|
6141
6140
|
except Exception:
|
|
6142
6141
|
pass
|
|
6143
6142
|
|
|
6144
|
-
# c0ab14c1 Phase 2: open-promise time-awareness (what I owe / what I'm
|
|
6145
|
-
# waiting on, with overdue/elapsed minutes). Soft-fail; only added when
|
|
6146
|
-
# there's something open.
|
|
6147
|
-
try:
|
|
6148
|
-
_pa = _promise_awareness(api_key)
|
|
6149
|
-
if _pa:
|
|
6150
|
-
resp["time_awareness"] = _pa
|
|
6151
|
-
except Exception:
|
|
6152
|
-
pass
|
|
6153
6143
|
|
|
6154
6144
|
return resp
|
|
6155
6145
|
|
|
@@ -6724,115 +6714,6 @@ def _promise_expires_iso(eta_minutes: int) -> Optional[str]:
|
|
|
6724
6714
|
return (_dt.now(_tz.utc) + _td(minutes=eta_minutes)).isoformat()
|
|
6725
6715
|
|
|
6726
6716
|
|
|
6727
|
-
def _promise_awareness(api_key: str) -> Optional[Dict[str, Any]]:
|
|
6728
|
-
"""Compact time-awareness block for boot/wait payloads: what I owe + what I'm
|
|
6729
|
-
waiting on, with overdue/elapsed minutes. None when there's nothing open."""
|
|
6730
|
-
try:
|
|
6731
|
-
r = be.sb_rpc("mc_promises_involving", {
|
|
6732
|
-
"p_api_key": api_key,
|
|
6733
|
-
"p_agent_name": AGENT_NAME,
|
|
6734
|
-
})
|
|
6735
|
-
except Exception:
|
|
6736
|
-
return None
|
|
6737
|
-
if not isinstance(r, dict) or not r.get("ok"):
|
|
6738
|
-
return None
|
|
6739
|
-
owe = r.get("i_owe") or []
|
|
6740
|
-
waiting = r.get("waiting_on") or []
|
|
6741
|
-
if not owe and not waiting:
|
|
6742
|
-
return None
|
|
6743
|
-
return {
|
|
6744
|
-
"i_owe": owe,
|
|
6745
|
-
"waiting_on": waiting,
|
|
6746
|
-
"overdue_i_owe": sum(1 for p in owe if isinstance(p, dict) and p.get("overdue")),
|
|
6747
|
-
"overdue_waiting_on": sum(1 for p in waiting if isinstance(p, dict) and p.get("overdue")),
|
|
6748
|
-
"hint": "Act on overdue items you owe; for items you're waiting on, the server is already nudging the responsible agent. meshcode_fulfill(promise_id) when done.",
|
|
6749
|
-
}
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
@mcp.tool()
|
|
6753
|
-
@with_working_status
|
|
6754
|
-
def meshcode_promise(to: str, what: str, eta_minutes: int) -> Dict[str, Any]:
|
|
6755
|
-
"""Register a time-bound commitment YOU are responsible for: "I'll do <what>
|
|
6756
|
-
for <to> within <eta_minutes>". If the ETA passes unfulfilled, the server
|
|
6757
|
-
proactively pings YOU (exponential backoff, auto-mutes) so nothing stalls
|
|
6758
|
-
waiting on you. Call meshcode_fulfill(promise_id) when done.
|
|
6759
|
-
|
|
6760
|
-
Use this whenever you tell another agent an ETA. Horizon cap = 24h
|
|
6761
|
-
(1440 min) — for longer commitments create a task instead.
|
|
6762
|
-
|
|
6763
|
-
Args:
|
|
6764
|
-
to: the agent you're committing to (the waiter / beneficiary).
|
|
6765
|
-
what: short description of what you'll deliver.
|
|
6766
|
-
eta_minutes: minutes from now until you expect to deliver (1..1440).
|
|
6767
|
-
"""
|
|
6768
|
-
if not to or not to.strip():
|
|
6769
|
-
return {"ok": False, "error": "to (recipient agent) is required"}
|
|
6770
|
-
if not what or not what.strip():
|
|
6771
|
-
return {"ok": False, "error": "what (promise text) is required"}
|
|
6772
|
-
expires = _promise_expires_iso(eta_minutes)
|
|
6773
|
-
if expires is None:
|
|
6774
|
-
return {"ok": False, "error": "eta_minutes must be an integer 1..1440 (24h); use a task for longer"}
|
|
6775
|
-
return be.sb_rpc("mc_record_promise", {
|
|
6776
|
-
"p_api_key": _get_api_key(),
|
|
6777
|
-
"p_to_target": to.strip(),
|
|
6778
|
-
"p_text": what.strip(),
|
|
6779
|
-
"p_expires_at": expires,
|
|
6780
|
-
"p_source_msg_id": None,
|
|
6781
|
-
"p_agent_name": AGENT_NAME, # responsible = me
|
|
6782
|
-
})
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
@mcp.tool()
|
|
6786
|
-
@with_working_status
|
|
6787
|
-
def meshcode_expect(of: str, what: str, eta_minutes: int) -> Dict[str, Any]:
|
|
6788
|
-
"""Register that you are WAITING on another agent: "<of> will do <what>
|
|
6789
|
-
within <eta_minutes>". If it doesn't happen in time, the server nudges
|
|
6790
|
-
<of> (the RESPONSIBLE agent) — not you. This is the anti-stalemate path:
|
|
6791
|
-
use it whenever you're blocked on someone else (e.g. "you push first, then
|
|
6792
|
-
I deploy"). Cancel with meshcode_fulfill(promise_id) once they deliver.
|
|
6793
|
-
|
|
6794
|
-
Args:
|
|
6795
|
-
of: the agent you're waiting on (the responsible party).
|
|
6796
|
-
what: short description of what you expect them to do.
|
|
6797
|
-
eta_minutes: minutes from now you expect it by (1..1440).
|
|
6798
|
-
"""
|
|
6799
|
-
if not of or not of.strip():
|
|
6800
|
-
return {"ok": False, "error": "of (agent you're waiting on) is required"}
|
|
6801
|
-
if not what or not what.strip():
|
|
6802
|
-
return {"ok": False, "error": "what (expectation text) is required"}
|
|
6803
|
-
expires = _promise_expires_iso(eta_minutes)
|
|
6804
|
-
if expires is None:
|
|
6805
|
-
return {"ok": False, "error": "eta_minutes must be an integer 1..1440 (24h); use a task for longer"}
|
|
6806
|
-
# responsible = `of`; waiter = me. Server nudges `of` when overdue.
|
|
6807
|
-
return be.sb_rpc("mc_record_promise", {
|
|
6808
|
-
"p_api_key": _get_api_key(),
|
|
6809
|
-
"p_to_target": AGENT_NAME,
|
|
6810
|
-
"p_text": what.strip(),
|
|
6811
|
-
"p_expires_at": expires,
|
|
6812
|
-
"p_source_msg_id": None,
|
|
6813
|
-
"p_agent_name": of.strip(), # responsible = the other agent
|
|
6814
|
-
})
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
@mcp.tool()
|
|
6818
|
-
@with_working_status
|
|
6819
|
-
def meshcode_fulfill(promise_id: str) -> Dict[str, Any]:
|
|
6820
|
-
"""Mark a promise/expectation fulfilled — stops all follow-up nudges
|
|
6821
|
-
immediately. Call this the moment the committed work is delivered (or the
|
|
6822
|
-
thing you were waiting on arrives).
|
|
6823
|
-
|
|
6824
|
-
Args:
|
|
6825
|
-
promise_id: the id returned by meshcode_promise / meshcode_expect (or
|
|
6826
|
-
surfaced in a promise_overdue nudge or the boot/wait time_awareness block).
|
|
6827
|
-
"""
|
|
6828
|
-
if not promise_id or not str(promise_id).strip():
|
|
6829
|
-
return {"ok": False, "error": "promise_id is required"}
|
|
6830
|
-
return be.sb_rpc("mc_fulfill_promise", {
|
|
6831
|
-
"p_api_key": _get_api_key(),
|
|
6832
|
-
"p_promise_id": str(promise_id).strip(),
|
|
6833
|
-
})
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
6717
|
# ----------------- PENDING APPROVALS (flagship: GO/no-go gate) -----------------
|
|
6837
6718
|
# Task bb396c9b. The agent asks for a GO/no-go before a gated action; a human (or an
|
|
6838
6719
|
# autonomous commander) approves/rejects with one button in the dashboard panel.
|
|
@@ -7363,120 +7244,6 @@ def meshcode_recall_search(query: str) -> Dict[str, Any]:
|
|
|
7363
7244
|
# mig 305-308 SQL substrate; mig 313 memory_hints contradictions + ranking;
|
|
7364
7245
|
# mig 314 public.* wrappers. Task b4befbfd.
|
|
7365
7246
|
|
|
7366
|
-
@mcp.tool()
|
|
7367
|
-
@with_working_status
|
|
7368
|
-
def meshcode_decide(
|
|
7369
|
-
decision: str,
|
|
7370
|
-
alternatives: Optional[List[str]] = None,
|
|
7371
|
-
evidence_refs: Optional[List[Dict[str, Any]]] = None,
|
|
7372
|
-
tier: str = "episodic",
|
|
7373
|
-
) -> Dict[str, Any]:
|
|
7374
|
-
"""Log a non-trivial decision to mc_traces (architectural choice, risky migration,
|
|
7375
|
-
scope decision). Returns {trace_id}. Pair with meshcode_outcome later when the
|
|
7376
|
-
result becomes observable so the team can learn from outcomes.
|
|
7377
|
-
|
|
7378
|
-
Args:
|
|
7379
|
-
decision: What you decided (one sentence).
|
|
7380
|
-
alternatives: Other options you considered (list of strings).
|
|
7381
|
-
evidence_refs: Pointers to evidence (list of {type, id|key} dicts).
|
|
7382
|
-
type values: 'memory','msg','task','file','rpc'.
|
|
7383
|
-
tier: 'episodic' (default) | 'reference' | 'critical'.
|
|
7384
|
-
"""
|
|
7385
|
-
if not decision or not decision.strip():
|
|
7386
|
-
return {"ok": False, "error": "decision required"}
|
|
7387
|
-
# MESH-IMPROVE-9 soft-gate: warn if a prior trace is missing its outcome
|
|
7388
|
-
# >24h. v1 = warning only (continues). v2 followup tightens to refuse.
|
|
7389
|
-
api_key = _get_api_key()
|
|
7390
|
-
warning = None
|
|
7391
|
-
try:
|
|
7392
|
-
_gate = be.sb_rpc("mc_check_unwritten_outcome", {
|
|
7393
|
-
"p_api_key": api_key,
|
|
7394
|
-
"p_project_id": _PROJECT_ID,
|
|
7395
|
-
"p_agent_name": AGENT_NAME,
|
|
7396
|
-
"p_age_hours": 24,
|
|
7397
|
-
})
|
|
7398
|
-
if isinstance(_gate, dict) and _gate.get("ok") and _gate.get("has_unwritten"):
|
|
7399
|
-
warning = {
|
|
7400
|
-
"type": "unwritten_outcome",
|
|
7401
|
-
"open_trace_id": _gate.get("oldest_trace_id"),
|
|
7402
|
-
"open_trace_decision": _gate.get("decision"),
|
|
7403
|
-
"open_trace_age_seconds": _gate.get("age_seconds"),
|
|
7404
|
-
"hint": _gate.get("hint"),
|
|
7405
|
-
"note": "soft-gate v1: this WARNS but does not block. v2 will refuse. Close prior trace with meshcode_outcome().",
|
|
7406
|
-
}
|
|
7407
|
-
except Exception:
|
|
7408
|
-
pass # Soft-fail when mig 340 not yet deployed
|
|
7409
|
-
result = be.sb_rpc("mc_decide", {
|
|
7410
|
-
"p_api_key": api_key,
|
|
7411
|
-
"p_project_id": _PROJECT_ID,
|
|
7412
|
-
"p_agent_name": AGENT_NAME,
|
|
7413
|
-
"p_decision": decision.strip(),
|
|
7414
|
-
"p_alternatives": json.dumps(alternatives or []),
|
|
7415
|
-
"p_evidence_refs": json.dumps(evidence_refs or []),
|
|
7416
|
-
"p_tier": tier,
|
|
7417
|
-
})
|
|
7418
|
-
if warning and isinstance(result, dict):
|
|
7419
|
-
result["outcome_gate_warning"] = warning
|
|
7420
|
-
return result
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
@mcp.tool()
|
|
7424
|
-
@with_working_status
|
|
7425
|
-
def meshcode_outcome(
|
|
7426
|
-
trace_id: str,
|
|
7427
|
-
outcome: str,
|
|
7428
|
-
outcome_eval: str,
|
|
7429
|
-
) -> Dict[str, Any]:
|
|
7430
|
-
"""Record the observable outcome of a prior decision (logged via meshcode_decide).
|
|
7431
|
-
Call this when results become visible — even partial results help future-you and
|
|
7432
|
-
the team learn what worked.
|
|
7433
|
-
|
|
7434
|
-
Args:
|
|
7435
|
-
trace_id: UUID returned by meshcode_decide.
|
|
7436
|
-
outcome: Free-form description of what happened.
|
|
7437
|
-
outcome_eval: 'success' | 'failure' | 'partial' | 'unknown'.
|
|
7438
|
-
"""
|
|
7439
|
-
if outcome_eval not in ("success", "failure", "partial", "unknown"):
|
|
7440
|
-
return {"ok": False, "error": "outcome_eval must be success|failure|partial|unknown"}
|
|
7441
|
-
return be.sb_rpc("mc_outcome", {
|
|
7442
|
-
"p_api_key": _get_api_key(),
|
|
7443
|
-
"p_project_id": _PROJECT_ID,
|
|
7444
|
-
"p_agent_name": AGENT_NAME,
|
|
7445
|
-
"p_trace_id": trace_id,
|
|
7446
|
-
"p_outcome": outcome,
|
|
7447
|
-
"p_outcome_eval": outcome_eval,
|
|
7448
|
-
})
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
@mcp.tool()
|
|
7452
|
-
@with_working_status
|
|
7453
|
-
def meshcode_explain(
|
|
7454
|
-
memory_key: Optional[str] = None,
|
|
7455
|
-
decision_substring: Optional[str] = None,
|
|
7456
|
-
trace_limit: int = 20,
|
|
7457
|
-
) -> Dict[str, Any]:
|
|
7458
|
-
"""Surface recent traces + open contradictions tied to a memory key or decision
|
|
7459
|
-
substring. Use when you want to understand why a memory ranks the way it does,
|
|
7460
|
-
or recall the chain of reasoning around a past choice. Exactly one of
|
|
7461
|
-
(memory_key, decision_substring) is required.
|
|
7462
|
-
|
|
7463
|
-
Args:
|
|
7464
|
-
memory_key: Match traces whose evidence_refs reference this key.
|
|
7465
|
-
decision_substring: ILIKE match against the decision text.
|
|
7466
|
-
trace_limit: Cap on returned traces (1..200, default 20).
|
|
7467
|
-
"""
|
|
7468
|
-
if not memory_key and not decision_substring:
|
|
7469
|
-
return {"ok": False, "error": "one of (memory_key, decision_substring) required"}
|
|
7470
|
-
return be.sb_rpc("mc_explain", {
|
|
7471
|
-
"p_api_key": _get_api_key(),
|
|
7472
|
-
"p_project_id": _PROJECT_ID,
|
|
7473
|
-
"p_agent_name": AGENT_NAME,
|
|
7474
|
-
"p_memory_key": memory_key,
|
|
7475
|
-
"p_decision_substring": decision_substring,
|
|
7476
|
-
"p_trace_limit": trace_limit,
|
|
7477
|
-
})
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
7247
|
# ----------------- BUG REPORTING -----------------
|
|
7481
7248
|
|
|
7482
7249
|
@mcp.tool()
|
|
@@ -7845,6 +7612,24 @@ def run_server():
|
|
|
7845
7612
|
)
|
|
7846
7613
|
# Stash for the heartbeat thread to relay to commander on first beat.
|
|
7847
7614
|
os.environ["_MESHCODE_BOOT_DRIFT"] = f"{_installed}->{_loaded}"
|
|
7615
|
+
# R2-5 (2.11.117) boot drift self-check: the launcher stamps the version it
|
|
7616
|
+
# RESOLVED for this agent (pin else latest PyPI) into the .mcp.json env as
|
|
7617
|
+
# MESHCODE_EXPECTED_VERSION. If what actually loaded differs, the spawn env
|
|
7618
|
+
# is stale/mismatched — report it to the mesh (first-heartbeat relay picks
|
|
7619
|
+
# up the sentinel) instead of degrading into "weird behavior".
|
|
7620
|
+
_expected = os.environ.get("MESHCODE_EXPECTED_VERSION", "").strip()
|
|
7621
|
+
if _expected and _expected != _SDK_VERSION:
|
|
7622
|
+
print(
|
|
7623
|
+
f"[meshcode] WARN boot_version_drift: expected={_expected} "
|
|
7624
|
+
f"loaded={_SDK_VERSION} — this agent's spawn env does not serve the "
|
|
7625
|
+
f"version the launcher resolved. Check ~/.meshcode/envs/{_expected}.",
|
|
7626
|
+
file=sys.stderr,
|
|
7627
|
+
)
|
|
7628
|
+
_prev = os.environ.get("_MESHCODE_BOOT_DRIFT", "")
|
|
7629
|
+
os.environ["_MESHCODE_BOOT_DRIFT"] = (
|
|
7630
|
+
(_prev + " " if _prev else "")
|
|
7631
|
+
+ f"expected:{_expected}->loaded:{_SDK_VERSION}"
|
|
7632
|
+
)
|
|
7848
7633
|
if sys.platform == "win32":
|
|
7849
7634
|
# Windows can't execv-preserve stdio; auto-update defers to next boot.
|
|
7850
7635
|
# Always announce so Windows users know why their first launch may
|
|
@@ -886,12 +886,22 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
886
886
|
print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` to fix.", file=sys.stderr)
|
|
887
887
|
return 2
|
|
888
888
|
|
|
889
|
-
#
|
|
890
|
-
#
|
|
891
|
-
#
|
|
892
|
-
#
|
|
893
|
-
#
|
|
889
|
+
# R2-5 (2.11.117) boot-always-latest: resolve target version (explicit pin
|
|
890
|
+
# else latest PyPI — owner directive sammybenu 2026-06-09), guarantee an
|
|
891
|
+
# immutable ~/.meshcode/envs/<target>/ env serving exactly it, and point
|
|
892
|
+
# this workspace's .mcp.json at it. Live agents keep their old env until
|
|
893
|
+
# recycle (no in-place overwrite → no class#2 clobber). Soft-fail → legacy
|
|
894
|
+
# sync_agent_env path (2.11.109 env-mismatch storm fix) below.
|
|
895
|
+
_boot_env_ver = None
|
|
894
896
|
if not dry_run:
|
|
897
|
+
try:
|
|
898
|
+
_boot_env_ver = self_update.ensure_boot_env(mcp_json_path)
|
|
899
|
+
if _boot_env_ver:
|
|
900
|
+
print(f"[meshcode] agent env: meshcode {_boot_env_ver} "
|
|
901
|
+
f"(boot-always-latest)", file=sys.stderr)
|
|
902
|
+
except Exception:
|
|
903
|
+
_boot_env_ver = None
|
|
904
|
+
if not dry_run and not _boot_env_ver:
|
|
895
905
|
try:
|
|
896
906
|
_doc = json.loads(mcp_json_path.read_text(encoding="utf-8"))
|
|
897
907
|
for _srv in (_doc.get("mcpServers") or {}).values():
|
|
@@ -1112,6 +1122,43 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
|
|
|
1112
1122
|
else:
|
|
1113
1123
|
cmd = [effective_editor]
|
|
1114
1124
|
|
|
1125
|
+
# R2-7 Phase 1 (mig 467): per-agent model preference. settings.json "model"
|
|
1126
|
+
# wins (skip RPC); else mc_agent_model_pref -> append --model. Full soft-fail
|
|
1127
|
+
# (no pref / no creds / network error -> current behavior). 4s RPC timeout.
|
|
1128
|
+
try:
|
|
1129
|
+
_settings_model = None
|
|
1130
|
+
_sp = ws / ".claude" / "settings.json"
|
|
1131
|
+
if _sp.exists():
|
|
1132
|
+
_settings_model = (json.loads(_sp.read_text(encoding="utf-8")) or {}).get("model")
|
|
1133
|
+
if not _settings_model:
|
|
1134
|
+
_mcp_cfg = json.loads(mcp_json_path.read_text(encoding="utf-8"))
|
|
1135
|
+
_env = (next(iter((_mcp_cfg.get("mcpServers") or {}).values()), {}) or {}).get("env", {}) or {}
|
|
1136
|
+
_url = _env.get("SUPABASE_URL"); _anon = _env.get("SUPABASE_KEY")
|
|
1137
|
+
_proj = _env.get("MESHCODE_PROJECT"); _agent_n = _env.get("MESHCODE_AGENT")
|
|
1138
|
+
_ak = os.environ.get("MESHCODE_API_KEY")
|
|
1139
|
+
if not _ak:
|
|
1140
|
+
try:
|
|
1141
|
+
from meshcode import secrets as _sec
|
|
1142
|
+
_ak = _sec.get_api_key(profile=_env.get("MESHCODE_KEYCHAIN_PROFILE") or "default")
|
|
1143
|
+
except Exception:
|
|
1144
|
+
_ak = None
|
|
1145
|
+
if _url and _anon and _proj and _agent_n and _ak:
|
|
1146
|
+
import urllib.request as _u
|
|
1147
|
+
_req = _u.Request(
|
|
1148
|
+
_url.rstrip("/") + "/rest/v1/rpc/mc_agent_model_pref",
|
|
1149
|
+
data=json.dumps({"p_api_key": _ak, "p_project_name": _proj,
|
|
1150
|
+
"p_agent_name": _agent_n}).encode(),
|
|
1151
|
+
method="POST",
|
|
1152
|
+
headers={"apikey": _anon, "Authorization": "Bearer " + _anon,
|
|
1153
|
+
"Content-Type": "application/json"})
|
|
1154
|
+
with _u.urlopen(_req, timeout=4) as _resp:
|
|
1155
|
+
_pref = (json.loads(_resp.read().decode()) or {}).get("model_pref")
|
|
1156
|
+
if _pref:
|
|
1157
|
+
cmd += ["--model", _pref]
|
|
1158
|
+
print(f"[meshcode] Model pref (dashboard): {_pref}", file=sys.stderr)
|
|
1159
|
+
except Exception as _me:
|
|
1160
|
+
print(f"[meshcode] model-pref skipped: {_me}", file=sys.stderr)
|
|
1161
|
+
|
|
1115
1162
|
if mode == "bypass":
|
|
1116
1163
|
bypass_check_cmd = effective_editor if not pinned_version else "claude"
|
|
1117
1164
|
if pinned_version or _claude_supports_bypass(bypass_check_cmd):
|
|
@@ -456,10 +456,18 @@ def check_and_maybe_update(verbose: bool = False) -> None:
|
|
|
456
456
|
|
|
457
457
|
|
|
458
458
|
def _env_version(python_exe: str) -> Optional[str]:
|
|
459
|
-
"""meshcode.__version__ as seen by ANOTHER python env (the agent's MCP server).
|
|
459
|
+
"""meshcode.__version__ as seen by ANOTHER python env (the agent's MCP server).
|
|
460
|
+
|
|
461
|
+
-I (isolated mode): `python -c` prepends CWD to sys.path, so probing from a
|
|
462
|
+
dir that contains a meshcode/ package (the SDK repo, any vendored copy)
|
|
463
|
+
reports THAT tree's version instead of the env's installed one (caught live
|
|
464
|
+
2026-06-09: env build verify refused a correct env because the probe ran
|
|
465
|
+
from the .117 worktree). Isolated mode answers the only question this
|
|
466
|
+
function asks: what does THIS env serve.
|
|
467
|
+
"""
|
|
460
468
|
try:
|
|
461
469
|
out = subprocess.run(
|
|
462
|
-
[python_exe, "-c", "import meshcode,sys; sys.stdout.write(meshcode.__version__)"],
|
|
470
|
+
[python_exe, "-I", "-c", "import meshcode,sys; sys.stdout.write(meshcode.__version__)"],
|
|
463
471
|
capture_output=True, text=True, timeout=10).stdout.strip()
|
|
464
472
|
return out or None
|
|
465
473
|
except Exception:
|
|
@@ -495,6 +503,144 @@ def sync_agent_env(mcp_python: str, verbose: bool = False) -> None:
|
|
|
495
503
|
pass
|
|
496
504
|
|
|
497
505
|
|
|
506
|
+
# ============================================================
|
|
507
|
+
# R2-5 (2.11.117): versioned immutable envs + boot-always-latest
|
|
508
|
+
#
|
|
509
|
+
# Owner directive (sammybenu 2026-06-09T22:54Z): "al boot SIEMPRE
|
|
510
|
+
# auto-update meshcode a la versión más nueva de PyPI" — per-agent pin
|
|
511
|
+
# only when explicitly set.
|
|
512
|
+
#
|
|
513
|
+
# Instead of upgrading a SHARED live env in place (class#2 clobber —
|
|
514
|
+
# the reason the 8e4f93f9 guard defers forever on a 24/7 fleet), each
|
|
515
|
+
# version gets its own immutable venv under ~/.meshcode/envs/<version>/
|
|
516
|
+
# built exactly once and never touched again. Running agents keep
|
|
517
|
+
# importing their old env until recycle; new spawns point at the
|
|
518
|
+
# resolved target. No overwrite → no clobber → no deferral.
|
|
519
|
+
# ============================================================
|
|
520
|
+
|
|
521
|
+
ENVS_DIR = STATE_DIR / "envs"
|
|
522
|
+
ENV_BUILD_TIMEOUT_SEC = 240
|
|
523
|
+
_ENV_OK_MARKER = ".build-ok"
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def resolve_target_version() -> Optional[str]:
|
|
527
|
+
"""Version an agent spawn should run. Resolution order:
|
|
528
|
+
|
|
529
|
+
1. MESHCODE_VERSION_PIN env var (explicit pin wins)
|
|
530
|
+
2. prefs key "version_pin" (explicit pin wins)
|
|
531
|
+
3. latest PyPI (1.5s probe) (the owner-directive default)
|
|
532
|
+
4. None → caller keeps current behavior (offline-safe)
|
|
533
|
+
"""
|
|
534
|
+
pin = os.environ.get("MESHCODE_VERSION_PIN") or _read_prefs().get("version_pin")
|
|
535
|
+
if pin:
|
|
536
|
+
return str(pin).strip()
|
|
537
|
+
return fetch_latest_version()
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _env_python(version: str) -> Path:
|
|
541
|
+
sub = "Scripts" if sys.platform == "win32" else "bin"
|
|
542
|
+
exe = "python.exe" if sys.platform == "win32" else "python3"
|
|
543
|
+
return ENVS_DIR / version / sub / exe
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def ensure_versioned_env(version: str, verbose: bool = True) -> Optional[Path]:
|
|
547
|
+
"""Create-once immutable venv for `version`; return its python, or None.
|
|
548
|
+
|
|
549
|
+
Build is atomic: venv + pip install + import-verify happen in a
|
|
550
|
+
.tmp-<version>-<pid> dir which is renamed into place only after the
|
|
551
|
+
installed version is VERIFIED == target (refuse-stale-spawn at the
|
|
552
|
+
source). Concurrent builders race benignly — first rename wins, the
|
|
553
|
+
loser falls back to the winner's env. Never raises.
|
|
554
|
+
"""
|
|
555
|
+
try:
|
|
556
|
+
final = ENVS_DIR / version
|
|
557
|
+
py = _env_python(version)
|
|
558
|
+
if py.exists() and (final / _ENV_OK_MARKER).exists():
|
|
559
|
+
return py
|
|
560
|
+
|
|
561
|
+
tmp = ENVS_DIR / f".tmp-{version}-{os.getpid()}"
|
|
562
|
+
tmp_py = Path(str(py).replace(str(final), str(tmp), 1))
|
|
563
|
+
ENVS_DIR.mkdir(parents=True, exist_ok=True)
|
|
564
|
+
if verbose:
|
|
565
|
+
print(f"[meshcode] building env for meshcode {version} (one-time, ~30s)...",
|
|
566
|
+
file=sys.stderr)
|
|
567
|
+
import shutil
|
|
568
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
569
|
+
if subprocess.run([sys.executable, "-m", "venv", str(tmp)],
|
|
570
|
+
capture_output=True, timeout=ENV_BUILD_TIMEOUT_SEC).returncode != 0:
|
|
571
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
572
|
+
return None
|
|
573
|
+
proc = subprocess.run(
|
|
574
|
+
[str(tmp_py), "-m", "pip", "install", "--no-cache-dir", "--quiet",
|
|
575
|
+
"--disable-pip-version-check", f"{PKG_NAME}=={version}"],
|
|
576
|
+
capture_output=True, timeout=ENV_BUILD_TIMEOUT_SEC)
|
|
577
|
+
if proc.returncode != 0:
|
|
578
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
579
|
+
return None
|
|
580
|
+
got = _env_version(str(tmp_py))
|
|
581
|
+
if got != version: # refuse-stale-spawn: never bless a wrong env
|
|
582
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
583
|
+
return None
|
|
584
|
+
(tmp / _ENV_OK_MARKER).write_text(version, encoding="utf-8")
|
|
585
|
+
try:
|
|
586
|
+
os.rename(tmp, final)
|
|
587
|
+
except OSError:
|
|
588
|
+
shutil.rmtree(tmp, ignore_errors=True) # concurrent builder won
|
|
589
|
+
if py.exists() and (final / _ENV_OK_MARKER).exists():
|
|
590
|
+
return py
|
|
591
|
+
return None
|
|
592
|
+
except Exception:
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def ensure_boot_env(mcp_json_path, verbose: bool = True) -> Optional[str]:
|
|
597
|
+
"""Boot-always-latest engine, called by `meshcode run` pre-editor-exec.
|
|
598
|
+
|
|
599
|
+
Resolves the target version (pin else latest PyPI), guarantees an
|
|
600
|
+
immutable env serving EXACTLY it, points the workspace .mcp.json
|
|
601
|
+
command there, and stamps MESHCODE_EXPECTED_VERSION into the server
|
|
602
|
+
env block (the serve compares it against its loaded __version__ at
|
|
603
|
+
boot and reports boot_version_drift to the mesh on mismatch).
|
|
604
|
+
|
|
605
|
+
Returns the target version on success, None on ANY failure — the
|
|
606
|
+
caller then keeps the legacy sync_agent_env path. Never raises.
|
|
607
|
+
"""
|
|
608
|
+
try:
|
|
609
|
+
if update_disabled() or is_editable_install():
|
|
610
|
+
return None
|
|
611
|
+
mcp_json_path = Path(mcp_json_path)
|
|
612
|
+
target = resolve_target_version()
|
|
613
|
+
if not target:
|
|
614
|
+
return None
|
|
615
|
+
doc = json.loads(mcp_json_path.read_text(encoding="utf-8"))
|
|
616
|
+
servers = doc.get("mcpServers") or {}
|
|
617
|
+
if not servers:
|
|
618
|
+
return None
|
|
619
|
+
srv = next(iter(servers.values()))
|
|
620
|
+
cur_cmd = srv.get("command") or ""
|
|
621
|
+
cur_ver = _env_version(cur_cmd) if cur_cmd else None
|
|
622
|
+
|
|
623
|
+
changed = False
|
|
624
|
+
if cur_ver != target:
|
|
625
|
+
py = ensure_versioned_env(target, verbose=verbose)
|
|
626
|
+
if py is None:
|
|
627
|
+
return None
|
|
628
|
+
if cur_cmd != str(py):
|
|
629
|
+
srv["command"] = str(py)
|
|
630
|
+
changed = True
|
|
631
|
+
env_block = srv.setdefault("env", {})
|
|
632
|
+
if env_block.get("MESHCODE_EXPECTED_VERSION") != target:
|
|
633
|
+
env_block["MESHCODE_EXPECTED_VERSION"] = target
|
|
634
|
+
changed = True
|
|
635
|
+
if changed:
|
|
636
|
+
tmp = mcp_json_path.with_name(mcp_json_path.name + ".tmp")
|
|
637
|
+
tmp.write_text(json.dumps(doc, indent=2), encoding="utf-8")
|
|
638
|
+
tmp.replace(mcp_json_path)
|
|
639
|
+
return target
|
|
640
|
+
except Exception:
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
|
|
498
644
|
# ============================================================
|
|
499
645
|
# Blocking variant — used by `meshcode run` to guarantee the editor
|
|
500
646
|
# subprocess inherits the latest meshcode_mcp/server.py on disk.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|