@agentunion/kite 1.3.2 → 1.4.0

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +200 -0
  2. package/cli.js +76 -0
  3. package/extensions/agents/assistant/entry.py +111 -1
  4. package/extensions/agents/assistant/server.py +263 -215
  5. package/extensions/channels/acp_channel/entry.py +111 -1
  6. package/extensions/channels/acp_channel/module.md +23 -22
  7. package/extensions/channels/acp_channel/server.py +263 -215
  8. package/extensions/event_hub_bench/entry.py +107 -1
  9. package/extensions/services/backup/entry.py +299 -21
  10. package/extensions/services/backup/module.md +24 -22
  11. package/extensions/services/model_service/entry.py +145 -19
  12. package/extensions/services/model_service/module.md +21 -22
  13. package/extensions/services/watchdog/entry.py +188 -25
  14. package/extensions/services/watchdog/monitor.py +144 -34
  15. package/extensions/services/web/WEBSOCKET_STATUS.md +143 -0
  16. package/extensions/services/web/config_example.py +35 -0
  17. package/extensions/services/web/config_loader.py +110 -0
  18. package/extensions/services/web/entry.py +114 -26
  19. package/extensions/services/web/module.md +35 -24
  20. package/extensions/services/web/pairing.py +250 -0
  21. package/extensions/services/web/pairing_codes.jsonl +16 -0
  22. package/extensions/services/web/relay.py +643 -0
  23. package/extensions/services/web/relay_config.json5 +67 -0
  24. package/extensions/services/web/routes/routes_management_ws.py +127 -0
  25. package/extensions/services/web/routes/routes_rpc.py +89 -0
  26. package/extensions/services/web/routes/routes_test.py +61 -0
  27. package/extensions/services/web/routes/schemas.py +0 -22
  28. package/extensions/services/web/server.py +421 -98
  29. package/extensions/services/web/static/css/style.css +67 -28
  30. package/extensions/services/web/static/index.html +234 -44
  31. package/extensions/services/web/static/js/app.js +1335 -48
  32. package/extensions/services/web/static/js/kernel-client-example.js +161 -0
  33. package/extensions/services/web/static/js/kernel-client.js +383 -0
  34. package/extensions/services/web/static/js/registry-tests.js +558 -0
  35. package/extensions/services/web/static/js/token-manager.js +175 -0
  36. package/extensions/services/web/static/pairing.html +248 -0
  37. package/extensions/services/web/static/test_registry.html +262 -0
  38. package/extensions/services/web/web_config.json5 +29 -0
  39. package/kernel/entry.py +120 -32
  40. package/kernel/event_hub.py +141 -16
  41. package/kernel/module.md +36 -33
  42. package/kernel/registry_store.py +48 -15
  43. package/kernel/rpc_router.py +120 -53
  44. package/kernel/server.py +219 -12
  45. package/kite_cli/__init__.py +3 -0
  46. package/kite_cli/__main__.py +5 -0
  47. package/kite_cli/commands/__init__.py +1 -0
  48. package/kite_cli/commands/clean.py +101 -0
  49. package/kite_cli/commands/doctor.py +35 -0
  50. package/kite_cli/commands/history.py +111 -0
  51. package/kite_cli/commands/info.py +96 -0
  52. package/kite_cli/commands/install.py +313 -0
  53. package/kite_cli/commands/list.py +143 -0
  54. package/kite_cli/commands/log.py +81 -0
  55. package/kite_cli/commands/rollback.py +88 -0
  56. package/kite_cli/commands/search.py +73 -0
  57. package/kite_cli/commands/uninstall.py +85 -0
  58. package/kite_cli/commands/update.py +118 -0
  59. package/kite_cli/core/__init__.py +1 -0
  60. package/kite_cli/core/checker.py +142 -0
  61. package/kite_cli/core/dependency.py +229 -0
  62. package/kite_cli/core/downloader.py +209 -0
  63. package/kite_cli/core/install_info.py +40 -0
  64. package/kite_cli/core/tool_installer.py +397 -0
  65. package/kite_cli/core/validator.py +78 -0
  66. package/kite_cli/main.py +289 -0
  67. package/kite_cli/utils/__init__.py +1 -0
  68. package/kite_cli/utils/i18n.py +252 -0
  69. package/kite_cli/utils/interactive.py +63 -0
  70. package/kite_cli/utils/operation_log.py +77 -0
  71. package/kite_cli/utils/paths.py +34 -0
  72. package/kite_cli/utils/version.py +308 -0
  73. package/launcher/entry.py +819 -158
  74. package/launcher/logging_setup.py +104 -0
  75. package/launcher/module.md +37 -37
  76. package/package.json +2 -1
  77. package/scripts/plan_manager.py +315 -0
  78. package/extensions/services/web/routes/routes_modules.py +0 -249
@@ -24,6 +24,7 @@ class RegistryStore:
24
24
  self.ttl = 60 # seconds before marking offline
25
25
  self.heartbeat_interval = 30
26
26
  self.is_debug = os.environ.get("KITE_DEBUG") == "1"
27
+ self.last_update_time = time.time() # Global registry update timestamp
27
28
 
28
29
  # ── Token verification ──
29
30
 
@@ -57,45 +58,82 @@ class RegistryStore:
57
58
  _REQUIRED_FIELDS = ("module_id", "module_type") # api_endpoint now optional
58
59
 
59
60
  def register_module(self, data: dict) -> dict:
60
- """Register or update a module. Idempotent — same module_id overwrites."""
61
+ """Register or update a module. Idempotent — same module_id overwrites.
62
+ Returns dict with ttl, heartbeat_interval, and changed flag on success, raises exception on failure."""
61
63
  # Validate required fields
62
64
  missing = [f for f in self._REQUIRED_FIELDS if not data.get(f)]
63
65
  if missing:
64
- return {"ok": False, "error": f"Missing required fields: {', '.join(missing)}"}
66
+ raise ValueError(f"Missing required fields: {', '.join(missing)}")
65
67
 
66
68
  mid = data["module_id"]
67
69
 
68
- # Warn on tool name conflicts
70
+ # Validate nested dict structure for lookup-able fields
71
+ # Only check known wrapper fields (tools, hooks, events_publish)
72
+ # RPC methods are registered directly at top level with their actual names
73
+ # events_subscribe is a list, not a dict
74
+ for field_name in ["tools", "hooks", "events_publish"]:
75
+ field_value = data.get(field_name)
76
+ if field_value is not None:
77
+ if not isinstance(field_value, dict):
78
+ raise TypeError(f"Field '{field_name}' must be a dict (nested structure), got {type(field_value).__name__}")
79
+ # Check if it's a flat dict with dot-notation keys (common mistake)
80
+ if any("." in str(k) for k in field_value.keys()):
81
+ raise ValueError(
82
+ f"Field '{field_name}' contains dot-notation keys (e.g., 'module.config.get'). "
83
+ f"Use nested dict structure instead: {{'module': {{'config': {{'get': ...}}}}}}. "
84
+ f"See docs/模块开发指南.md section 4.3 for correct format."
85
+ )
86
+
87
+ # Warn on tool name conflicts (skip 'rpc' — it's a standard tool every module registers)
69
88
  new_tools = data.get("tools", {})
70
89
  if isinstance(new_tools, dict):
71
90
  for tool_name in new_tools:
91
+ if tool_name == "rpc":
92
+ continue
72
93
  for other_mid, other_data in self.modules.items():
73
94
  if other_mid == mid:
74
95
  continue
75
96
  if tool_name in other_data.get("tools", {}):
76
97
  print(f"[kernel] WARNING: tool '{tool_name}' registered by both '{other_mid}' and '{mid}'")
77
98
 
99
+ # Check if content changed (compare with existing registration)
100
+ changed = True
101
+ old_record = self.modules.get(mid)
102
+ if old_record:
103
+ # Strip action field from new data
104
+ new_record = {k: v for k, v in data.items() if k != "action"}
105
+ # Compare without status and registered_at fields
106
+ old_data = {k: v for k, v in old_record.items() if k not in ("status", "registered_at")}
107
+ changed = (new_record != old_data)
108
+
78
109
  # Strip action field — it's a request verb, not part of the registration payload
79
110
  record = {k: v for k, v in data.items() if k != "action"}
80
111
  record["status"] = "registered" # State machine: connected → registered (via register RPC)
81
112
  record["registered_at"] = time.time()
82
113
  self.modules[mid] = record
83
114
  self.heartbeats[mid] = time.time()
84
- return {"ok": True, "ttl": self.ttl, "heartbeat_interval": self.heartbeat_interval}
115
+
116
+ # Update global timestamp if content changed
117
+ if changed:
118
+ self.last_update_time = time.time()
119
+
120
+ return {"ttl": self.ttl, "heartbeat_interval": self.heartbeat_interval, "changed": changed}
85
121
 
86
122
  def deregister_module(self, module_id: str) -> dict:
87
- """Remove a module record immediately."""
123
+ """Remove a module record immediately. Returns empty dict."""
88
124
  self.modules.pop(module_id, None)
89
125
  self.heartbeats.pop(module_id, None)
90
- return {"ok": True}
126
+ # Update global timestamp
127
+ self.last_update_time = time.time()
128
+ return {}
91
129
 
92
130
  def heartbeat(self, module_id: str) -> dict:
93
- """Renew heartbeat for a module."""
131
+ """Renew heartbeat for a module. Returns empty dict on success, raises exception on failure."""
94
132
  if module_id not in self.modules:
95
- return {"ok": False, "error": "module not registered"}
133
+ raise KeyError(f"Module '{module_id}' not registered")
96
134
  self.heartbeats[module_id] = time.time()
97
135
  # Don't change status — heartbeat just keeps alive, doesn't upgrade state
98
- return {"ok": True}
136
+ return {}
99
137
 
100
138
  def set_connected(self, module_id: str):
101
139
  """Mark a module as connected (WS established, not yet registered).
@@ -177,7 +215,7 @@ class RegistryStore:
177
215
  def lookup(self, field: str = None, module: str = None, value: str = None) -> list[dict]:
178
216
  """
179
217
  Search across all online modules. All three params support glob patterns.
180
- Returns list of {field, module, api_endpoint, value}.
218
+ Returns list of {field, module, value}.
181
219
  """
182
220
  results = []
183
221
  for mid, data in self.modules.items():
@@ -186,8 +224,6 @@ class RegistryStore:
186
224
  if module and not fnmatch.fnmatch(mid, module):
187
225
  continue
188
226
 
189
- api_ep = data.get("api_endpoint", "")
190
-
191
227
  if field:
192
228
  matches = self._match_fields(data, field)
193
229
  for fpath, fval in matches:
@@ -196,7 +232,6 @@ class RegistryStore:
196
232
  results.append({
197
233
  "field": fpath,
198
234
  "module": mid,
199
- "api_endpoint": api_ep,
200
235
  "value": fval,
201
236
  })
202
237
  elif value:
@@ -205,14 +240,12 @@ class RegistryStore:
205
240
  results.append({
206
241
  "field": k,
207
242
  "module": mid,
208
- "api_endpoint": api_ep,
209
243
  "value": v,
210
244
  })
211
245
  else:
212
246
  results.append({
213
247
  "field": "module_id",
214
248
  "module": mid,
215
- "api_endpoint": api_ep,
216
249
  "value": mid,
217
250
  })
218
251
 
@@ -73,6 +73,12 @@ class RpcRouter:
73
73
  self.connections = connections
74
74
  self.kernel_server = kernel_server # Direct reference to KernelServer
75
75
 
76
+ # RPC 统计计数器
77
+ self._rpc_total = 0 # 总 RPC 调用次数
78
+ self._rpc_builtin = 0 # 内置方法调用次数
79
+ self._rpc_forwarded = 0 # 转发调用次数
80
+ self._rpc_errors = 0 # 错误次数
81
+
76
82
  # Builtin method dispatch table
77
83
  self.methods: dict[str, callable] = {
78
84
  "registry.register": self._registry_register,
@@ -89,7 +95,6 @@ class RpcRouter:
89
95
  "kernel.health": self._kernel_health,
90
96
  "kernel.generate_tokens": self._kernel_generate_tokens,
91
97
  "kernel.register_tokens": self._kernel_register_tokens,
92
- "kernel.shutdown": self._kernel_shutdown,
93
98
  }
94
99
 
95
100
  # Pending cross-module forwards: internal_id -> PendingForward
@@ -102,6 +107,9 @@ class RpcRouter:
102
107
  method = msg.get("method", "")
103
108
  msg_id = msg.get("id")
104
109
 
110
+ # 统计:总调用次数
111
+ self._rpc_total += 1
112
+
105
113
  # JSON-RPC Notification (no id) — currently not handled from clients
106
114
  if msg_id is None:
107
115
  return
@@ -111,12 +119,27 @@ class RpcRouter:
111
119
  # Builtin method
112
120
  handler = self.methods.get(method)
113
121
  if handler:
122
+ # 统计:内置方法调用
123
+ self._rpc_builtin += 1
114
124
  try:
115
125
  result = await handler(caller_id, params)
116
- await ws.send_text(_result_msg(msg_id, result))
117
126
  except Exception as e:
127
+ # 统计:错误次数
128
+ self._rpc_errors += 1
118
129
  print(f"[kernel] RPC handler error ({method}): {e}")
119
- await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
130
+ try:
131
+ await ws.send_text(_error_msg(msg_id, INTERNAL_ERROR, str(e)))
132
+ except Exception:
133
+ # Can't send error response, connection closed
134
+ pass
135
+ return
136
+
137
+ # Send result (may fail if connection closed during shutdown)
138
+ try:
139
+ await ws.send_text(_result_msg(msg_id, result))
140
+ except Exception as e:
141
+ # Connection closed during shutdown — this is normal, exit silently
142
+ pass
120
143
  return
121
144
 
122
145
  # Cross-module forward: method prefix is target module_id
@@ -126,19 +149,27 @@ class RpcRouter:
126
149
  if target in self.connections:
127
150
  # Check if target module is ready (state machine protection)
128
151
  if not self.registry.is_ready(target):
152
+ # 统计:错误次数
153
+ self._rpc_errors += 1
129
154
  await ws.send_text(_error_msg(
130
155
  msg_id, MODULE_OFFLINE,
131
156
  f"Module not ready: {target} (status: {self.registry.modules.get(target, {}).get('status', 'unknown')})"))
132
157
  return
158
+ # 统计:转发调用
159
+ self._rpc_forwarded += 1
133
160
  await self._forward(caller_id, ws, msg_id, target, method, params)
134
161
  return
135
162
  # Target not connected — check if registered but offline
136
163
  if target in self.registry.modules:
164
+ # 统计:错误次数
165
+ self._rpc_errors += 1
137
166
  await ws.send_text(_error_msg(
138
167
  msg_id, MODULE_OFFLINE, f"Module offline: {target}"))
139
168
  return
140
169
 
141
170
  # Method not found
171
+ # 统计:错误次数
172
+ self._rpc_errors += 1
142
173
  await ws.send_text(_error_msg(msg_id, METHOD_NOT_FOUND, f"Method not found: {method}"))
143
174
 
144
175
  async def handle_response(self, module_id: str, msg: dict):
@@ -243,62 +274,102 @@ class RpcRouter:
243
274
  async def _registry_register(self, caller_id: str, params: dict) -> dict:
244
275
  mid = params.get("module_id")
245
276
  if not mid:
246
- return {"ok": False, "error": "module_id required"}
277
+ raise ValueError("module_id required")
247
278
  # Permission: only Launcher or the module itself
248
279
  if caller_id != "launcher" and caller_id != mid:
249
- return {"ok": False, "error": f"Module '{caller_id}' cannot register as '{mid}'"}
280
+ raise PermissionError(f"Module '{caller_id}' cannot register as '{mid}'")
281
+
282
+ print(f"[kernel] registry.register called by {caller_id} for module {mid}")
283
+ print(f"[kernel] rpc_methods: {params.get('rpc_methods')}")
284
+
250
285
  result = self.registry.register_module(params)
251
- if result.get("ok"):
252
- self.event_hub.publish_internal("module.registered", {"module_id": mid})
286
+ self.event_hub.publish_internal("module.registered", {"module_id": mid}, source=self.kernel_server.module_id)
287
+
288
+ # Only publish registry.updated if content actually changed
289
+ if result.get("changed", True):
290
+ from datetime import datetime, timezone
291
+ self.event_hub.publish_internal("registry.updated", {
292
+ "module_id": mid,
293
+ "timestamp": datetime.now(timezone.utc).isoformat(),
294
+ "action": "register",
295
+ }, source=self.kernel_server.module_id)
296
+ print(f"[kernel] registry.updated published for {mid}")
297
+ else:
298
+ print(f"[kernel] {mid} re-registered with no changes, skipping registry.updated")
299
+
300
+ # When Launcher registers, Kernel publishes its own module.ready
301
+ if mid == "launcher" and self.kernel_server:
302
+ self.kernel_server.publish_ready()
303
+ print(f"[kernel] launcher registered → kernel module.ready published")
253
304
 
254
- # When Launcher registers, Kernel publishes its own module.ready
255
- if mid == "launcher" and self.kernel_server:
256
- self.kernel_server.publish_ready()
257
- print(f"[kernel] launcher registered → kernel module.ready published")
258
305
  return result
259
306
 
260
307
  async def _registry_deregister(self, caller_id: str, params: dict) -> dict:
261
308
  mid = params.get("module_id")
262
309
  if not mid:
263
- return {"ok": False, "error": "module_id required"}
310
+ raise ValueError("module_id required")
264
311
  if caller_id != "launcher" and caller_id != mid:
265
- return {"ok": False, "error": f"Module '{caller_id}' cannot deregister '{mid}'"}
266
- result = self.registry.deregister_module(mid)
267
- if result.get("ok"):
268
- self.event_hub.publish_internal("module.unregistered", {"module_id": mid})
269
- return result
312
+ raise PermissionError(f"Module '{caller_id}' cannot deregister '{mid}'")
313
+
314
+ self.registry.deregister_module(mid)
315
+ self.event_hub.publish_internal("module.unregistered", {"module_id": mid}, source=self.kernel_server.module_id)
316
+
317
+ # Publish registry.updated event for cache invalidation
318
+ from datetime import datetime, timezone
319
+ self.event_hub.publish_internal("registry.updated", {
320
+ "module_id": mid,
321
+ "timestamp": datetime.now(timezone.utc).isoformat(),
322
+ "action": "deregister",
323
+ }, source=self.kernel_server.module_id)
324
+
325
+ return {}
270
326
 
271
327
  async def _registry_heartbeat(self, caller_id: str, params: dict) -> dict:
272
328
  mid = params.get("module_id")
273
329
  if not mid:
274
- return {"ok": False, "error": "module_id required"}
330
+ raise ValueError("module_id required")
275
331
  if caller_id != "launcher" and caller_id != mid:
276
- return {"ok": False, "error": f"Module '{caller_id}' cannot heartbeat for '{mid}'"}
332
+ raise PermissionError(f"Module '{caller_id}' cannot heartbeat for '{mid}'")
333
+
277
334
  return self.registry.heartbeat(mid)
278
335
 
279
336
  async def _registry_lookup(self, caller_id: str, params: dict) -> dict:
337
+ field = params.get("field")
338
+ module = params.get("module")
339
+ value = params.get("value")
340
+
341
+ print(f"[kernel] registry.lookup called by {caller_id}: field={field}, module={module}, value={value}")
342
+
280
343
  results = self.registry.lookup(
281
- field=params.get("field"),
282
- module=params.get("module"),
283
- value=params.get("value"),
344
+ field=field,
345
+ module=module,
346
+ value=value,
284
347
  )
285
- return {"ok": True, "results": results}
348
+
349
+ print(f"[kernel] registry.lookup results: {len(results)} matches")
350
+ for r in results:
351
+ print(f"[kernel] - {r['module']}.{r['field']} = {r['value']}")
352
+
353
+ return {
354
+ "results": results,
355
+ "last_update_time": self.registry.last_update_time
356
+ }
286
357
 
287
358
  async def _registry_get(self, caller_id: str, params: dict) -> dict:
288
359
  path = params.get("path", "")
289
360
  if not path:
290
- return {"ok": False, "error": "path required"}
361
+ raise ValueError("path required")
291
362
  val, found = self.registry.get_by_path(path)
292
363
  if not found:
293
- return {"ok": False, "error": f"Path not found: {path}"}
294
- return {"ok": True, "value": val}
364
+ raise KeyError(f"Path not found: {path}")
365
+ return {"value": val}
295
366
 
296
367
  async def _registry_verify(self, caller_id: str, params: dict) -> dict:
297
368
  token = params.get("token", "")
298
369
  module_id = self.registry.verify_token(token)
299
- if module_id:
300
- return {"ok": True, "module_id": module_id}
301
- return {"ok": False}
370
+ if not module_id:
371
+ raise PermissionError("Invalid token")
372
+ return {"module_id": module_id}
302
373
 
303
374
  # ── Builtin handlers: event.* ──
304
375
 
@@ -318,16 +389,15 @@ class RpcRouter:
318
389
  async def _event_subscribe(self, caller_id: str, params: dict) -> dict:
319
390
  events = params.get("events", [])
320
391
  if not isinstance(events, list) or not events:
321
- return {"ok": False, "error": "events must be a non-empty list"}
392
+ raise ValueError("events must be a non-empty list")
322
393
  self.event_hub.handle_subscribe(caller_id, events)
323
- return {"ok": True}
394
+ return {}
324
395
 
325
396
  async def _event_unsubscribe(self, caller_id: str, params: dict) -> dict:
326
397
  events = params.get("events", [])
327
398
  if not isinstance(events, list) or not events:
328
- return {"ok": False, "error": "events must be a non-empty list"}
329
- self.event_hub.handle_unsubscribe(caller_id, events)
330
- return {"ok": True}
399
+ raise ValueError("events must be a non-empty list")
400
+ return self.event_hub.handle_unsubscribe(caller_id, events)
331
401
 
332
402
  # ── Builtin handlers: kernel.* ──
333
403
 
@@ -335,7 +405,16 @@ class RpcRouter:
335
405
  return {"pong": True, "timestamp": datetime.now(timezone.utc).isoformat()}
336
406
 
337
407
  async def _kernel_stats(self, caller_id: str, params: dict) -> dict:
338
- return self.event_hub.get_stats()
408
+ event_stats = self.event_hub.get_stats()
409
+ return {
410
+ **event_stats,
411
+ "rpc": {
412
+ "total": self._rpc_total,
413
+ "builtin": self._rpc_builtin,
414
+ "forwarded": self._rpc_forwarded,
415
+ "errors": self._rpc_errors
416
+ }
417
+ }
339
418
 
340
419
  async def _kernel_health(self, caller_id: str, params: dict) -> dict:
341
420
  eh_health = self.event_hub.get_health()
@@ -356,15 +435,15 @@ class RpcRouter:
356
435
  params: {"modules": ["mod1", "mod2", ...]}
357
436
 
358
437
  Returns:
359
- {"ok": True, "tokens": {"mod1": "token1", "mod2": "token2", ...}}
438
+ {"tokens": {"mod1": "token1", "mod2": "token2", ...}}
360
439
  """
361
440
  # Only Launcher may request token generation
362
441
  if caller_id != "launcher":
363
- return {"ok": False, "error": "Only Launcher may generate tokens"}
442
+ raise PermissionError("Only Launcher may generate tokens")
364
443
 
365
444
  modules = params.get("modules", [])
366
445
  if not isinstance(modules, list):
367
- return {"ok": False, "error": "modules must be a list"}
446
+ raise ValueError("modules must be a list")
368
447
 
369
448
  import secrets
370
449
  tokens = {}
@@ -374,25 +453,13 @@ class RpcRouter:
374
453
  # Register tokens in registry
375
454
  self.registry.register_tokens(tokens)
376
455
 
377
- return {"ok": True, "tokens": tokens}
456
+ return {"tokens": tokens}
378
457
 
379
458
  async def _kernel_register_tokens(self, caller_id: str, params: dict) -> dict:
380
459
  # Only Launcher may register tokens
381
460
  if caller_id != "launcher":
382
- return {"ok": False, "error": "Only Launcher may register tokens"}
461
+ raise PermissionError("Only Launcher may register tokens")
383
462
  self.registry.register_tokens(params)
384
- return {"ok": True}
385
-
386
- async def _kernel_shutdown(self, caller_id: str, params: dict) -> dict:
387
- """Shutdown Kernel. Only Launcher may call this."""
388
- if caller_id != "launcher":
389
- return {"ok": False, "error": "Only Launcher may shutdown Kernel"}
390
-
391
- print("[kernel] Received shutdown request from Launcher")
392
-
393
- # Schedule shutdown (don't block RPC response)
394
- if self.kernel_server:
395
- asyncio.create_task(self.kernel_server.shutdown())
463
+ return {}
396
464
 
397
- return {"ok": True}
398
465