ltcai 10.2.0 → 10.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 (149) hide show
  1. package/README.md +40 -44
  2. package/docs/CHANGELOG.md +125 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MYPY_BACKLOG.md +65 -0
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +1 -1
  8. package/docs/SURFACE_PARITY.md +33 -6
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/context.py +23 -3
  14. package/lattice_brain/graph/_kg_common.py +164 -9
  15. package/lattice_brain/graph/_kg_contract.py +223 -0
  16. package/lattice_brain/graph/_kg_fsutil.py +2 -2
  17. package/lattice_brain/graph/curator.py +4 -4
  18. package/lattice_brain/graph/discovery.py +12 -1
  19. package/lattice_brain/graph/discovery_index.py +12 -1
  20. package/lattice_brain/graph/documents.py +17 -2
  21. package/lattice_brain/graph/ingest.py +12 -1
  22. package/lattice_brain/graph/proactive.py +9 -5
  23. package/lattice_brain/graph/projection.py +12 -1
  24. package/lattice_brain/graph/provenance.py +12 -1
  25. package/lattice_brain/graph/retrieval.py +12 -1
  26. package/lattice_brain/graph/retrieval_docgen.py +12 -1
  27. package/lattice_brain/graph/retrieval_reads.py +12 -1
  28. package/lattice_brain/graph/retrieval_vector.py +12 -1
  29. package/lattice_brain/graph/schema.py +6 -2
  30. package/lattice_brain/graph/write_master.py +12 -1
  31. package/lattice_brain/ingestion.py +1 -1
  32. package/lattice_brain/quality.py +5 -5
  33. package/lattice_brain/runtime/agent_runtime.py +7 -2
  34. package/lattice_brain/runtime/contracts.py +2 -1
  35. package/lattice_brain/runtime/hooks.py +1 -1
  36. package/lattice_brain/runtime/multi_agent.py +1 -1
  37. package/lattice_brain/workflow.py +10 -7
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/admin.py +2 -2
  40. package/latticeai/api/chat.py +19 -15
  41. package/latticeai/api/chat_agent_http.py +19 -7
  42. package/latticeai/api/chat_helpers.py +9 -8
  43. package/latticeai/api/chat_stream.py +2 -2
  44. package/latticeai/api/computer_use.py +2 -2
  45. package/latticeai/api/hooks.py +4 -2
  46. package/latticeai/api/local_files.py +8 -4
  47. package/latticeai/api/mcp.py +2 -2
  48. package/latticeai/api/models.py +5 -5
  49. package/latticeai/api/permissions.py +2 -2
  50. package/latticeai/api/search.py +5 -3
  51. package/latticeai/api/security_dashboard.py +3 -5
  52. package/latticeai/api/setup.py +3 -3
  53. package/latticeai/api/static_routes.py +7 -2
  54. package/latticeai/api/tools.py +9 -4
  55. package/latticeai/api/workspace.py +25 -22
  56. package/latticeai/app_factory.py +132 -1346
  57. package/latticeai/cli/entrypoint.py +1 -1
  58. package/latticeai/core/agent.py +28 -14
  59. package/latticeai/core/audit.py +5 -3
  60. package/latticeai/core/embedding_providers.py +19 -6
  61. package/latticeai/core/legacy_compatibility.py +1 -1
  62. package/latticeai/core/marketplace.py +2 -2
  63. package/latticeai/core/mcp_catalog.py +3 -1
  64. package/latticeai/core/model_compat.py +1 -1
  65. package/latticeai/core/plugins.py +1 -1
  66. package/latticeai/core/project_sessions.py +1 -1
  67. package/latticeai/core/run_explain.py +6 -4
  68. package/latticeai/core/workspace_os.py +0 -4
  69. package/latticeai/core/workspace_os_constants.py +1 -1
  70. package/latticeai/core/workspace_os_state.py +1 -1
  71. package/latticeai/core/workspace_snapshots.py +3 -3
  72. package/latticeai/integrations/telegram_bot.py +52 -3
  73. package/latticeai/models/router.py +61 -34
  74. package/latticeai/runtime/access_runtime.py +3 -1
  75. package/latticeai/runtime/build_phases.py +1417 -0
  76. package/latticeai/runtime/history_writer.py +138 -0
  77. package/latticeai/runtime/review_wiring.py +6 -1
  78. package/latticeai/runtime/runtime_context.py +276 -0
  79. package/latticeai/runtime/sso_config_runtime.py +1 -1
  80. package/latticeai/runtime/stages.py +8 -2
  81. package/latticeai/services/app_context.py +23 -0
  82. package/latticeai/services/architecture_readiness.py +1 -1
  83. package/latticeai/services/brain_intelligence.py +4 -2
  84. package/latticeai/services/cloud_streaming.py +5 -3
  85. package/latticeai/services/command_center.py +1 -1
  86. package/latticeai/services/evidence_actions.py +2 -1
  87. package/latticeai/services/hybrid_chat.py +1 -1
  88. package/latticeai/services/local_knowledge.py +25 -9
  89. package/latticeai/services/memory_service.py +8 -4
  90. package/latticeai/services/model_catalog.py +12 -12
  91. package/latticeai/services/model_engines.py +15 -17
  92. package/latticeai/services/model_loading.py +1 -1
  93. package/latticeai/services/model_runtime.py +31 -33
  94. package/latticeai/services/multimodal_streaming.py +10 -4
  95. package/latticeai/services/openai_compatible_adapter.py +4 -3
  96. package/latticeai/services/product_readiness.py +1 -1
  97. package/latticeai/services/run_executor.py +12 -3
  98. package/latticeai/services/search_service.py +1 -1
  99. package/latticeai/services/triggers.py +1 -1
  100. package/latticeai/setup/auto_setup.py +1 -1
  101. package/latticeai/setup/wizard.py +12 -3
  102. package/latticeai/tools/computer.py +3 -1
  103. package/latticeai/tools/documents.py +2 -2
  104. package/package.json +4 -2
  105. package/scripts/check_current_release_docs.mjs +1 -1
  106. package/src-tauri/Cargo.lock +1 -1
  107. package/src-tauri/Cargo.toml +1 -1
  108. package/src-tauri/tauri.conf.json +1 -1
  109. package/static/app/asset-manifest.json +37 -37
  110. package/static/app/assets/{Act-CbdGD-2i.js → Act-BK__Y5VE.js} +1 -1
  111. package/static/app/assets/{AdminConsole-LgCkXpnf.js → AdminConsole-ZrTyN8_F.js} +1 -1
  112. package/static/app/assets/{Brain-CoPGJI1L.js → Brain-CYHDXpPB.js} +1 -1
  113. package/static/app/assets/{BrainHome-gVnaxSwp.js → BrainHome-bbJulb5g.js} +1 -1
  114. package/static/app/assets/{BrainSignals-ChxAYHtj.js → BrainSignals-CesSPtUP.js} +1 -1
  115. package/static/app/assets/{Capture-DBtgkHZg.js → Capture-p5upXmz2.js} +1 -1
  116. package/static/app/assets/{CommandPalette-Ovtv5I0Y.js → CommandPalette-0zkyC0RY.js} +1 -1
  117. package/static/app/assets/Library-Dt-vABI2.js +1 -0
  118. package/static/app/assets/{LivingBrain-CNz-6NSd.js → LivingBrain-Bz-h6LBH.js} +1 -1
  119. package/static/app/assets/{ProductFlow-ePX-Y73J.js → ProductFlow-DAZ7DJCE.js} +1 -1
  120. package/static/app/assets/{ReviewCard-C4mpvkwH.js → ReviewCard-Bv0WGbY6.js} +1 -1
  121. package/static/app/assets/{System-BvWNK1zc.js → System-BhrJQSD_.js} +1 -1
  122. package/static/app/assets/{activity-CiauPV_3.js → activity-AolpZJCH.js} +1 -1
  123. package/static/app/assets/{bot-Dwct-Q1x.js → bot-CsVsT2nd.js} +1 -1
  124. package/static/app/assets/{brain-BKDW1F0T.js → brain-vuppco5y.js} +1 -1
  125. package/static/app/assets/{button-CCB9Kuw7.js → button-1BP1PJZ3.js} +1 -1
  126. package/static/app/assets/{circle-pause-DNa8WHC5.js → circle-pause-5paeCOtq.js} +1 -1
  127. package/static/app/assets/{circle-play-8jmxn5mf.js → circle-play-By8mFYRV.js} +1 -1
  128. package/static/app/assets/{cpu-mxvF3V1I.js → cpu-DvUQxdKe.js} +1 -1
  129. package/static/app/assets/{download--SmCcx0p.js → download-CFqYG90a.js} +1 -1
  130. package/static/app/assets/{folder-open-D4Wy5roo.js → folder-open-CVSnp-Dp.js} +1 -1
  131. package/static/app/assets/{hard-drive-BwQcSPlS.js → hard-drive-Ckr4Vd6-.js} +1 -1
  132. package/static/app/assets/{index-DDV2YZwM.js → index--0zKF5Ow.js} +3 -3
  133. package/static/app/assets/index-CEkL4_M-.css +2 -0
  134. package/static/app/assets/{input-DFhSjmLS.js → input-D-JKquXU.js} +1 -1
  135. package/static/app/assets/{network-Bts7VO94.js → network-DB88FZ2T.js} +1 -1
  136. package/static/app/assets/{primitives-BuSMEJY8.js → primitives-D8U9OaIo.js} +1 -1
  137. package/static/app/assets/search-B8jsVTw_.js +1 -0
  138. package/static/app/assets/{shield-alert-BfTO6X_G.js → shield-alert-DnsvO79P.js} +1 -1
  139. package/static/app/assets/{textarea-BjctW1oh.js → textarea-4RchwbTa.js} +1 -1
  140. package/static/app/assets/{useFocusTrap-CE43-LrA.js → useFocusTrap-CFJjy7cS.js} +1 -1
  141. package/static/app/assets/{useQuery-Bv3ejLL5.js → useQuery-3Reu-A4a.js} +1 -1
  142. package/static/app/assets/{users-CsNqLZAj.js → users-BTxYksxa.js} +1 -1
  143. package/static/app/assets/{utils-KFFdVG_t.js → utils-D02Vf-fw.js} +1 -1
  144. package/static/app/assets/{workspace-wdCvdyPF.js → workspace-CSJNgX-C.js} +1 -1
  145. package/static/app/index.html +4 -4
  146. package/static/sw.js +1 -1
  147. package/static/app/assets/Library-CBia2Fvm.js +0 -1
  148. package/static/app/assets/index-CQWdDU3z.css +0 -2
  149. package/static/app/assets/search-DZzxhWaQ.js +0 -1
@@ -360,7 +360,7 @@ def create_mcp_router(
360
360
  require_user(request)
361
361
  mcp_registry._SKILLS_MARKETPLACE_FETCHED_AT = None
362
362
  skills = await _fetch_skills_marketplace()
363
- by_author = {}
363
+ by_author: Dict[str, int] = {}
364
364
  for s in skills:
365
365
  by_author[s["author"]] = by_author.get(s["author"], 0) + 1
366
366
  return {"status": "ok", "total": len(skills), "by_author": by_author}
@@ -399,7 +399,7 @@ def create_mcp_router(
399
399
  require_user(request)
400
400
  mcp_registry._PLUGIN_DIRECTORY_FETCHED_AT = None
401
401
  plugins = await _fetch_plugin_directory()
402
- by_license = {}
402
+ by_license: Dict[str, int] = {}
403
403
  for p in plugins:
404
404
  lic = p.get("license", "unknown")
405
405
  by_license[lic] = by_license.get(lic, 0) + 1
@@ -155,25 +155,25 @@ def create_models_router(
155
155
 
156
156
  def _recommended_with_engine_options(
157
157
  items: List[Dict[str, object]],
158
- engines: Optional[List[Dict[str, object]]] = None,
158
+ engines: Optional[List[Dict[str, Any]]] = None,
159
159
  loaded_ids: Optional[List[str]] = None,
160
160
  current_id: Optional[str] = None,
161
161
  ) -> List[Dict[str, object]]:
162
162
  from latticeai.core.model_compat import model_runtime_compatibility
163
163
 
164
164
  engine_lookup = {str(engine.get("id") or ""): engine for engine in engines or []}
165
- model_lookup: Dict[str, Dict[str, object]] = {}
165
+ model_lookup: Dict[str, Dict[str, Any]] = {}
166
166
  for engine in engines or []:
167
167
  engine_id = str(engine.get("id") or "")
168
- for model in engine.get("models") or []:
168
+ for model in list(engine.get("models") or []):
169
169
  if isinstance(model, dict):
170
170
  model_lookup[str(model.get("id") or "")] = {**model, "_engine": engine_id}
171
171
  loaded = set(loaded_ids or [])
172
- out: List[Dict[str, object]] = []
172
+ out: List[Dict[str, Any]] = []
173
173
  for item in items:
174
174
  short_id = str(item["id"]).lower()
175
175
  aliases = MODEL_ENGINE_ALIASES.get(short_id) or {}
176
- options: List[Dict[str, object]] = []
176
+ options: List[Dict[str, Any]] = []
177
177
  for engine_name in ("local_mlx", "ollama", "lmstudio", "llamacpp", "vllm"):
178
178
  real = aliases.get(engine_name)
179
179
  if not real:
@@ -11,7 +11,7 @@ import threading
11
11
  import time
12
12
  import urllib.request
13
13
  from pathlib import Path
14
- from typing import Dict, Optional, Tuple
14
+ from typing import Any, Dict, Optional, Tuple
15
15
 
16
16
  from fastapi import APIRouter, HTTPException, Request
17
17
 
@@ -33,7 +33,7 @@ class PermissionGateway:
33
33
  self.local_approval_ttl_seconds = 5 * 60
34
34
  self.local_approval_lock = threading.Lock()
35
35
  self.perm_queue_lock = threading.Lock()
36
- self.local_approvals: Dict[str, Dict[str, object]] = {}
36
+ self.local_approvals: Dict[str, Dict[str, Any]] = {}
37
37
  self.discord_permission_webhook_url = config.discord_permission_webhook
38
38
  self.discord_bot_token = config.discord_bot_token
39
39
  self.discord_permission_channel = config.discord_permission_channel
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Callable, Dict, Optional
5
+ from typing import Any, Callable, Dict, NoReturn, Optional
6
6
 
7
7
  from fastapi import APIRouter, HTTPException, Request
8
8
  from pydantic import BaseModel, Field
@@ -84,14 +84,16 @@ def create_search_router(
84
84
  ) -> APIRouter:
85
85
  router = APIRouter()
86
86
 
87
- def _guarded(request: Request) -> SearchService:
87
+ def _guarded(request: Request) -> "_ScopedSearchService":
88
88
  user = require_user(request)
89
89
  allowed = None
90
90
  if allowed_workspaces_for is not None and user:
91
91
  allowed = allowed_workspaces_for(user)
92
92
  return _ScopedSearchService(service, allowed)
93
93
 
94
- def _raise_graph_error(exc: Exception) -> None:
94
+ def _raise_graph_error(exc: Exception) -> NoReturn:
95
+ # NoReturn, not None: every handler ends with this in its except
96
+ # branch, and without it each one reads as a missing return.
95
97
  raise HTTPException(status_code=404, detail=str(exc)) from exc
96
98
 
97
99
  @router.post("/api/search/hybrid")
@@ -515,8 +515,6 @@ def create_security_router(
515
515
  else:
516
516
  raise HTTPException(status_code=400, detail="지원하지 않는 scope입니다.")
517
517
 
518
- if not isinstance(rows, list):
519
- rows = []
520
518
 
521
519
  _log_view(admin_email, "export", f"{scope}:{fmt}", reason="export")
522
520
 
@@ -530,7 +528,7 @@ def create_security_router(
530
528
  headers={"Content-Disposition": f"attachment; filename={filename}"},
531
529
  )
532
530
  if fmt == "csv":
533
- body = _csv_dump(rows)
531
+ body = _csv_dump(rows) # type: ignore[assignment]
534
532
  filename = f"security_{scope}.csv"
535
533
  return Response(
536
534
  content=body,
@@ -538,7 +536,7 @@ def create_security_router(
538
536
  headers={"Content-Disposition": f"attachment; filename={filename}"},
539
537
  )
540
538
  if fmt in {"excel", "xlsx"}:
541
- body = _excel_dump(rows)
539
+ body = _excel_dump(rows) # type: ignore[assignment]
542
540
  filename = f"security_{scope}.xlsx"
543
541
  return Response(
544
542
  content=body,
@@ -547,7 +545,7 @@ def create_security_router(
547
545
  )
548
546
  if fmt == "pdf":
549
547
  overview = build_sensitivity_report(get_history() or []).get("summary", {}) or {}
550
- body = _pdf_report("Lattice AI Security Report", rows, overview)
548
+ body = _pdf_report("Lattice AI Security Report", rows, overview) # type: ignore[assignment]
551
549
  filename = f"security_{scope}.pdf"
552
550
  return Response(
553
551
  content=body,
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Dict, List, Optional
5
+ from typing import Any, Dict, List, Optional
6
6
 
7
7
  from fastapi import APIRouter, HTTPException, Request
8
8
  from fastapi.responses import StreamingResponse
@@ -63,7 +63,7 @@ def create_setup_router(
63
63
 
64
64
  # ── Setup Wizard ─────────────────────────────────────────────────────────────
65
65
 
66
- def setup_auto_state() -> Dict[str, object]:
66
+ def setup_auto_state() -> Dict[str, Any]:
67
67
  """Return the PPT-aligned zero-config setup state used by setup UI/API."""
68
68
  profile = auto_setup_probe()
69
69
  recommendation = auto_setup_recommend(profile)
@@ -77,7 +77,7 @@ def create_setup_router(
77
77
  }
78
78
 
79
79
 
80
- def primary_setup_model(recs: Dict[str, object]) -> Optional[Dict[str, object]]:
80
+ def primary_setup_model(recs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
81
81
  models = recs.get("models") if isinstance(recs, dict) else None
82
82
  if not isinstance(models, list):
83
83
  return None
@@ -9,7 +9,7 @@ import subprocess
9
9
  import time
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
- from typing import Callable, Optional
12
+ from typing import Any, Callable, Dict, Optional
13
13
 
14
14
  from fastapi import APIRouter, Cookie, HTTPException, Request
15
15
  from fastapi.responses import FileResponse, HTMLResponse
@@ -253,7 +253,12 @@ def create_static_routes_router(
253
253
  """CPU / RAM / GPU(MLX) 사용량을 반환합니다."""
254
254
  require_user(request)
255
255
  import re as _re
256
- result = {"cpu_pct": 0.0, "ram_pct": 0.0, "gpu_mem_pct": 0.0, "gpu_mem_gb": 0.0}
256
+ result: Dict[str, Any] = {
257
+ "cpu_pct": 0.0,
258
+ "ram_pct": 0.0,
259
+ "gpu_mem_pct": 0.0,
260
+ "gpu_mem_gb": 0.0,
261
+ }
257
262
  try:
258
263
  # CPU
259
264
  top_out = subprocess.run(["top", "-l", "1", "-n", "0"], capture_output=True, text=True, timeout=4).stdout
@@ -243,10 +243,15 @@ def create_tools_router(
243
243
  api_router = APIRouter()
244
244
  HOOKS = hooks
245
245
  CONFIG = config
246
- DATA_DIR = data_dir
247
- STATIC_DIR = static_dir
246
+ # These three are Optional in the signature only because the legacy
247
+ # kwarg form predates AppContext; the router cannot be built without
248
+ # them, so resolve once here rather than at every use.
249
+ if data_dir is None or static_dir is None:
250
+ raise RuntimeError("create_tools_router requires data_dir and static_dir")
251
+ DATA_DIR: Path = data_dir
252
+ STATIC_DIR: Path = static_dir
248
253
  router = model_router
249
- ENABLE_GRAPH = enable_graph
254
+ ENABLE_GRAPH: bool = bool(enable_graph)
250
255
  KNOWLEDGE_GRAPH = knowledge_graph
251
256
  LOCAL_KG_WATCHER = local_kg_watcher
252
257
  _require_graph = require_graph
@@ -327,7 +332,7 @@ def create_tools_router(
327
332
  or None
328
333
  )
329
334
 
330
- def _knowledge_scope(request: Request, current_user: str, *, write: bool) -> Dict[str, str]:
335
+ def _knowledge_scope(request: Request, current_user: str, *, write: bool) -> Dict[str, Any]:
331
336
  # Preserve the historical shared vault only for explicit single-user,
332
337
  # no-auth local mode. Authenticated deployments always partition by
333
338
  # both authorized workspace and account.
@@ -165,35 +165,38 @@ def create_workspace_router(context: AppContext) -> APIRouter:
165
165
  router = APIRouter()
166
166
 
167
167
  # Bind injected deps to the names the moved handler bodies expect.
168
- service = context.workspace_service
169
- require_user = context.require_user
170
- require_admin = context.require_admin
171
- get_current_user = context.get_current_user
172
- append_audit_event = context.append_audit_event
173
- get_history = context.get_history
174
- get_audit_log = context.get_audit_log
175
- load_users = context.load_users
176
- scan_environment = context.scan_environment
177
- local_sysinfo = context.local_sysinfo
178
- get_recommendations = context.get_recommendations
179
- install_skill = context.install_skill
180
- remove_skill_directory = context.remove_skill_directory
181
- redact_secret_text = context.redact_secret_text
182
- capability_registry = context.capability_registry
168
+ # `context.require(...)` for anything this router calls: an absent
169
+ # dependency now names itself here instead of surfacing as
170
+ # "'NoneType' object is not callable" inside a request handler.
171
+ service = context.require("workspace_service")
172
+ require_user = context.require("require_user")
173
+ require_admin = context.require("require_admin")
174
+ get_current_user = context.require("get_current_user")
175
+ append_audit_event = context.require("append_audit_event")
176
+ get_history = context.require("get_history")
177
+ get_audit_log = context.require("get_audit_log")
178
+ load_users = context.require("load_users")
179
+ scan_environment = context.require("scan_environment")
180
+ local_sysinfo = context.require("local_sysinfo")
181
+ get_recommendations = context.require("get_recommendations")
182
+ install_skill = context.require("install_skill")
183
+ remove_skill_directory = context.require("remove_skill_directory")
184
+ redact_secret_text = context.require("redact_secret_text")
185
+ capability_registry = context.require("capability_registry")
183
186
 
184
187
  svc = service
185
188
  WORKSPACE_OS = service.store
186
- _workspace_graph = context.workspace_graph
187
- _graph_stats_safe = context.graph_stats
188
- _workspace_models_payload = context.workspace_models
189
- _workspace_settings_payload = context.workspace_settings
190
- _require_graph = context.require_graph
189
+ _workspace_graph = context.require("workspace_graph")
190
+ _graph_stats_safe = context.require("graph_stats")
191
+ _workspace_models_payload = context.require("workspace_models")
192
+ _workspace_settings_payload = context.require("workspace_settings")
193
+ _require_graph = context.require("require_graph")
191
194
  KNOWLEDGE_GRAPH = context.knowledge_graph
192
195
  LOCAL_KG_WATCHER = context.local_kg_watcher
193
196
  SKILLS_DIR = context.skills_dir
194
197
  LOCAL_MODEL = context.local_model
195
198
  PUBLIC_MODEL = context.public_model
196
- _fetch_skills_marketplace = context.fetch_skills_marketplace
199
+ _fetch_skills_marketplace = context.require("fetch_skills_marketplace")
197
200
  _workspace_scope = _workspace_scope_from_request
198
201
 
199
202
  def _gate_read(request: Request):
@@ -651,7 +654,7 @@ def create_workspace_router(context: AppContext) -> APIRouter:
651
654
  @router.get("/workspace/vscode/status")
652
655
  async def workspace_vscode_status(request: Request):
653
656
  require_user(request)
654
- last_seen = int(_VSCODE_STATUS.get("last_seen_ms") or 0)
657
+ last_seen = int(str(_VSCODE_STATUS.get("last_seen_ms") or 0))
655
658
  now_ms = int(datetime.utcnow().timestamp() * 1000)
656
659
  connected = last_seen > 0 and (now_ms - last_seen) < 60000
657
660
  return {