@agentunion/kite 1.5.0 → 1.6.1

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 (574) hide show
  1. package/.claude/skills/kite/checklists/feature-checklist.md +496 -0
  2. package/.claude/skills/kite/references/event-patterns.md +180 -0
  3. package/.claude/skills/kite/references/health-check.md +202 -0
  4. package/.claude/skills/kite/references/http-service.md +199 -0
  5. package/.claude/skills/kite/references/module-md-spec.md +172 -0
  6. package/.claude/skills/kite/references/multi-connection.md +147 -0
  7. package/.claude/skills/kite/references/rpc-patterns.md +199 -0
  8. package/.claude/skills/kite/references/shutdown-sequence.md +146 -0
  9. package/.claude/skills/kite/references/stdin-protocol.md +147 -0
  10. package/.claude/skills/kite/references/test-center-integration.md +178 -0
  11. package/.claude/skills/kite/references/ws-lifecycle.md +301 -0
  12. package/.claude/skills/kite/skill.md +272 -0
  13. package/.claude/skills/kite/templates/go/README.md +20 -0
  14. package/.claude/skills/kite/templates/node/entry.js +134 -0
  15. package/.claude/skills/kite/templates/node/module.md +16 -0
  16. package/.claude/skills/kite/templates/node/server.js +351 -0
  17. package/.claude/skills/kite/templates/node/server_http.js +90 -0
  18. package/.claude/skills/kite/templates/python/entry.py +425 -0
  19. package/.claude/skills/kite/templates/python/module.md +26 -0
  20. package/.claude/skills/kite/templates/python/server.py +447 -0
  21. package/.claude/skills/kite/templates/python/server_http.py +433 -0
  22. package/cli.js +38 -4
  23. package/core/env_checker.py +96 -0
  24. package/docs/05-/347/237/255/344/277/241/350/256/244/350/257/201/344/270/216/347/224/250/346/210/267/344/277/241/346/201/257/346/216/245/345/217/243/346/226/207/346/241/243.md +507 -0
  25. package/docs/ACP/345/215/217/350/256/256/345/205/274/345/256/271/346/226/271/346/241/210.md +138 -0
  26. package/docs/CI/344/270/216AI/350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/226/271/346/241/210.md +75 -0
  27. package/docs/CLI/345/274/200/345/217/221/350/256/241/345/210/222.md +595 -0
  28. package/docs/ClaudeCode/350/277/234/347/250/213/345/215/217/344/275/234/347/263/273/347/273/237-/346/212/200/346/234/257/350/257/204/344/274/260.md +535 -0
  29. package/docs/ClaudeCode/350/277/234/347/250/213/345/215/217/344/275/234/347/263/273/347/273/237/350/256/276/350/256/241.md +631 -0
  30. package/docs/Evol-App/344/275/277/347/224/250KernelClient/346/224/271/351/200/240/345/256/214/346/210/220.md +342 -0
  31. package/docs/Evol/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/346/246/202/350/246/201.md +604 -0
  32. package/docs/Evol/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/350/256/276/350/256/241.md +1708 -0
  33. package/docs/Evol/346/250/241/345/235/227/350/256/276/350/256/241/346/226/271/346/241/210.md +1154 -0
  34. package/docs/Evol/351/241/265/351/235/242/346/217/222/344/273/266/345/214/226-Evol/346/250/241/345/235/227/345/256/236/346/226/275/346/214/207/345/215/227.md +403 -0
  35. package/docs/Evol/351/241/265/351/235/242/346/217/222/344/273/266/345/214/226-/345/244/226/351/203/250/346/250/241/345/235/227/346/216/245/345/205/245/346/214/207/345/215/227.md +468 -0
  36. package/docs/HTTP-RPC/350/277/201/347/247/273/345/210/260WebSocket/350/256/241/345/210/222.md +318 -0
  37. package/docs/INDEX.md +388 -0
  38. package/docs/KITE_DOCS_GUIDE.md +33 -0
  39. package/docs/Kernel-Client-Kite-Token/346/224/257/346/214/201/345/256/236/346/226/275/345/256/214/346/210/220.md +330 -0
  40. package/docs/Kernel/344/270/273/345/212/250Ping/346/234/272/345/210/266-/346/255/243/347/241/256/345/256/236/347/216/260.md +235 -0
  41. package/docs/Kernel/344/270/273/345/212/250Ping/346/234/272/345/210/266/345/256/236/346/226/275/346/200/273/347/273/223.md +204 -0
  42. package/docs/Kite/345/256/211/350/243/205/351/227/256/351/242/230/350/247/243/345/206/263/346/226/271/346/241/210.md +362 -0
  43. package/docs/Kite/346/216/247/345/210/266/345/217/260/346/217/222/344/273/266/345/214/226/346/236/266/346/236/204/350/256/276/350/256/241-/347/273/210/346/236/201/347/233/256/346/240/207.md +721 -0
  44. package/docs/Kite/346/216/247/345/210/266/345/217/260/347/273/237/344/270/200WebSocket/346/224/271/351/200/240/346/226/271/346/241/210.md +821 -0
  45. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/01-/346/241/206/346/236/266/345/256/232/344/275/215.md +12 -0
  46. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/02-/346/240/270/345/277/203/346/246/202/345/277/265.md +341 -0
  47. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/03-/347/263/273/347/273/237/346/236/266/346/236/204.md +257 -0
  48. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/04-/346/250/241/345/235/227/350/247/204/350/214/203.md +263 -0
  49. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/05-/346/240/270/345/277/203/346/265/201/347/250/213-/346/226/260/347/211/210.md +267 -0
  50. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/05-/346/240/270/345/277/203/346/265/201/347/250/213.md +149 -0
  51. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/06-/347/233/256/345/275/225/347/273/223/346/236/204.md +231 -0
  52. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/07-/346/225/260/346/215/256/346/250/241/345/236/213.md +68 -0
  53. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/08-/346/211/251/345/261/225/346/200/247.md +34 -0
  54. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/09-/344/270/216/345/205/267/344/275/223/345/272/224/347/224/250/347/232/204/345/205/263/347/263/273.md +22 -0
  55. package/docs/Kite/346/241/206/346/236/266/350/256/276/350/256/241/README.md +46 -0
  56. package/docs/Kite/347/263/273/347/273/237/345/220/257/345/212/250/346/265/201/347/250/213.md +567 -0
  57. package/docs/Launcher/345/220/257/345/212/250/345/231/250/346/226/207/346/241/243.md +745 -0
  58. package/docs/Polyglot/350/277/220/350/241/214/346/227/266/344/270/216Clawdbot/345/205/274/345/256/271/346/200/247/350/256/276/350/256/241.md +321 -0
  59. package/docs/Redis/344/270/216/346/250/241/345/235/227/345/244/232/345/256/236/344/276/213/346/226/271/346/241/210.md +438 -0
  60. package/docs/Relay-Kite-Token/350/256/244/350/257/201/345/256/236/346/226/275/345/256/214/346/210/220.md +178 -0
  61. package/docs/Relay-Token/346/235/203/351/231/220/351/205/215/347/275/256/351/252/214/350/257/201.md +113 -0
  62. package/docs/Watchdog/345/201/245/345/272/267/346/243/200/346/237/245/344/270/216WebSocket-Ping/346/234/272/345/210/266/345/210/206/346/236/220.md +367 -0
  63. package/docs/Watchdog/350/265/204/346/272/220/347/233/221/346/216/247/347/255/226/347/225/245.md +92 -0
  64. package/docs/WebSocket/346/216/245/346/224/266/345/276/252/347/216/257/346/255/273/351/224/201/351/230/262/350/214/203/350/247/204/350/214/203.md +357 -0
  65. package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/344/270/216/351/207/215/350/277/236/346/234/272/345/210/266/345/256/214/346/225/264/346/226/271/346/241/210.md +531 -0
  66. package/docs/WebSocket/350/277/236/346/216/245/351/237/247/346/200/247/346/226/271/346/241/210.md +169 -0
  67. package/docs/WebSocket/351/207/215/350/277/236/346/234/272/345/210/266/346/265/213/350/257/225/346/212/245/345/221/212.md +169 -0
  68. package/docs/WebSocket/351/207/215/350/277/236/351/200/200/351/201/277/346/234/272/345/210/266/346/226/271/346/241/210.md +394 -0
  69. package/docs/Web/346/250/241/345/235/227/344/270/216Evol/346/250/241/345/235/227/351/207/215/346/236/204/345/210/206/346/236/220.md +521 -0
  70. package/docs/audit-api-guide.md +68 -0
  71. package/docs/audit-module-design.md +315 -0
  72. package/docs/audit-module-implementation-summary.md +149 -0
  73. package/docs/llm-context-design.md +52 -0
  74. package/docs/llm-test-enhancement-plan.md +970 -0
  75. package/docs/logs-api-guide.md +42 -0
  76. package/docs/npm/345/214/205Python/347/216/257/345/242/203/347/256/241/347/220/206/346/226/271/346/241/210.md +302 -0
  77. package/docs/npm/345/217/221/345/270/203/344/270/216CLI/344/275/277/347/224/250/346/214/207/345/215/227.md +245 -0
  78. package/docs/stdio/344/270/216/347/253/257/345/217/243/345/217/221/347/216/260/351/207/215/346/236/204.md +480 -0
  79. package/docs/web/346/250/241/345/235/227/344/270/255/350/275/254/346/234/215/345/212/241/350/256/276/350/256/241/346/226/271/346/241/210.md +449 -0
  80. package/docs//344/272/213/344/273/266/345/244/204/347/220/206/346/234/272/345/210/266.md +388 -0
  81. package/docs//344/272/213/344/273/266/345/244/204/347/220/206/350/247/204/350/214/203.md +113 -0
  82. package/docs//344/272/213/344/273/266/350/256/242/351/230/205/351/200/232/351/205/215/347/254/246/350/247/204/350/214/203.md +256 -0
  83. package/docs//344/272/213/344/273/266/351/230/237/345/210/227/345/274/271/346/200/247/347/256/241/347/220/206.md +449 -0
  84. package/docs//344/272/244/344/272/222/345/274/217/347/273/210/347/253/257/346/216/247/345/210/266/346/226/271/346/241/210.md +301 -0
  85. package/docs//344/273/243/347/220/206/345/220/257/345/212/250/345/231/250/344/270/216/345/256/271/345/231/250/345/214/226.md +140 -0
  86. package/docs//344/273/243/347/240/201/347/273/237/350/256/241/345/267/245/345/205/267/344/275/277/347/224/250/350/257/264/346/230/216.md +217 -0
  87. package/docs//344/274/230/351/233/205/351/200/200/345/207/272/350/247/204/350/214/203.md +362 -0
  88. package/docs//344/276/235/350/265/226/347/256/241/347/220/206/350/257/264/346/230/216.md +141 -0
  89. package/docs//344/277/256/345/244/215/346/235/203/351/231/220/351/227/256/351/242/230-evol-RPC/346/235/203/351/231/220.md +268 -0
  90. package/docs//345/210/240/351/231/244kernel-client-example/345/256/214/346/210/220.md +309 -0
  91. package/docs//345/210/240/351/231/244ws-management/345/256/214/346/210/220.md +418 -0
  92. package/docs//345/220/257/345/212/250/344/274/230/345/214/226/346/226/271/346/241/210.md +522 -0
  93. package/docs//345/220/257/345/212/250/344/276/235/350/265/226/344/270/216/346/216/222/345/272/217.md +105 -0
  94. package/docs//345/256/211/350/243/205/350/204/232/346/234/254/345/274/200/345/217/221/346/226/207/346/241/243.md +643 -0
  95. package/docs//345/256/214/346/225/264/345/220/257/345/212/250/346/265/201/347/250/213/350/256/276/350/256/241.md +452 -0
  96. package/docs//345/256/236/347/216/260/350/247/204/345/210/222.md +195 -0
  97. package/docs//345/277/203/350/267/263/346/234/272/345/210/266/351/207/215/346/236/204/346/200/273/347/273/223.md +166 -0
  98. package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210-/345/256/211/345/205/250/345/256/241/346/237/245.md +176 -0
  99. package/docs//346/217/241/346/211/213/350/256/244/350/257/201/346/226/271/346/241/210.md +908 -0
  100. package/docs//346/226/207/346/241/243/346/233/264/346/226/260/346/270/205/345/215/225.md +83 -0
  101. package/docs//346/227/245/345/277/227/344/270/216/345/274/202/345/270/270/345/244/204/347/220/206/350/247/204/350/214/203.md +829 -0
  102. package/docs//346/227/245/345/277/227/350/260/203/350/257/225/345/256/236/346/210/230/346/214/207/345/215/227.md +25 -0
  103. package/docs//346/236/266/346/236/204/345/200/237/351/211/264/346/214/207/345/215/227.md +977 -0
  104. package/docs//346/236/266/346/236/204/346/224/271/351/200/240-/345/256/214/346/210/220/346/200/273/347/273/223.md +440 -0
  105. package/docs//346/236/266/346/236/204/347/216/260/347/212/266/344/270/216/347/273/210/346/236/201/347/233/256/346/240/207/345/257/271/346/257/224/345/210/206/346/236/220.md +508 -0
  106. package/docs//346/250/241/345/235/227/345/244/232/350/277/236/346/216/245/346/216/247/345/210/266/347/255/226/347/225/245.md +220 -0
  107. package/docs//346/250/241/345/235/227/345/256/211/350/243/205/346/234/272/345/210/266/350/256/276/350/256/241.md +500 -0
  108. package/docs//346/250/241/345/235/227/345/274/200/345/217/221/346/214/207/345/215/227.md +1824 -0
  109. package/docs//346/250/241/345/235/227/347/203/255/346/233/264/346/226/260.md +89 -0
  110. package/docs//346/250/241/345/235/227/350/277/234/347/250/213/351/203/250/347/275/262/345/274/200/345/217/221/350/247/204/350/214/203.md +460 -0
  111. package/docs//346/250/241/345/235/227/351/200/200/345/207/272/346/234/272/345/210/266/345/256/214/346/225/264/346/226/271/346/241/210.md +303 -0
  112. package/docs//346/250/241/345/235/227/351/205/215/347/275/256/345/212/240/350/275/275/344/270/216/347/203/255/351/207/215/350/275/275/350/247/204/350/214/203.md +369 -0
  113. package/docs//346/265/213/350/257/225/344/270/255/345/277/203/346/267/273/345/212/240/346/250/241/345/235/227/346/265/213/350/257/225/346/214/207/345/215/227.md +147 -0
  114. package/docs//347/211/210/346/234/254/351/224/201/345/256/232/347/216/257/345/242/203/347/256/241/347/220/206/346/226/271/346/241/210.md +331 -0
  115. package/docs//347/216/257/345/242/203/345/217/230/351/207/217/344/270/216/350/277/220/350/241/214/346/227/266/347/233/256/345/275/225/350/256/276/350/256/241.md +499 -0
  116. package/docs//347/216/257/345/242/203/347/256/241/347/220/206/345/256/214/346/225/264/346/226/271/346/241/210.md +334 -0
  117. package/docs//350/231/232/346/213/237/346/250/241/345/235/227/344/270/255/350/275/254/346/234/215/345/212/241/345/256/214/346/225/264/350/256/276/350/256/241.md +1496 -0
  118. package/docs//350/231/232/346/213/237/347/216/257/345/242/203/345/267/245/344/275/234/345/216/237/347/220/206.md +163 -0
  119. package/docs//350/256/241/345/210/222/347/256/241/347/220/206/345/231/250/344/275/277/347/224/250/346/214/207/345/215/227.md +196 -0
  120. package/docs//350/256/244/350/257/201/346/250/241/345/235/227/344/270/216Gateway/350/256/276/350/256/241/346/226/271/346/241/210.md +765 -0
  121. package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241-/346/227/247/347/211/210.md +1117 -0
  122. package/docs//350/277/234/347/250/213/346/250/241/345/235/227/350/256/276/350/256/241.md +451 -0
  123. package/docs//351/207/215/346/236/204/346/234/272/345/210/266/346/270/205/345/215/225.md +192 -0
  124. package/docs//351/223/276/350/267/257/350/277/275/350/270/252/346/226/271/346/241/210.md +242 -0
  125. package/docs//351/231/215/347/272/247/347/255/226/347/225/245/350/256/276/350/256/241/346/226/271/346/241/210.md +618 -0
  126. package/extensions/agents/assistant/entry.py +113 -14
  127. package/extensions/agents/assistant/module.md +27 -22
  128. package/extensions/agents/assistant/server.py +291 -105
  129. package/extensions/channels/acp_channel/entry.py +114 -16
  130. package/extensions/channels/acp_channel/module.md +4 -0
  131. package/extensions/channels/acp_channel/server.py +396 -105
  132. package/extensions/channels/phone_channel/__init__.py +1 -0
  133. package/extensions/channels/phone_channel/entry.py +503 -0
  134. package/extensions/channels/phone_channel/module.md +31 -0
  135. package/extensions/channels/phone_channel/server.py +686 -0
  136. package/extensions/event_hub_bench/entry.py +55 -12
  137. package/extensions/event_hub_bench/module.md +27 -27
  138. package/extensions/services/audit/README.md +134 -0
  139. package/extensions/services/audit/collector.py +73 -0
  140. package/extensions/services/audit/entry.py +444 -0
  141. package/extensions/services/audit/module.md +66 -0
  142. package/extensions/services/audit/query_audit.py +111 -0
  143. package/extensions/services/audit/routes/__init__.py +1 -0
  144. package/extensions/services/audit/routes/routes_audit.py +113 -0
  145. package/extensions/services/audit/schemas/__init__.py +5 -0
  146. package/extensions/services/audit/schemas/audit_event.py +92 -0
  147. package/extensions/services/audit/server.py +542 -0
  148. package/extensions/services/audit/storage.py +95 -0
  149. package/extensions/services/auth/entry.py +1054 -0
  150. package/extensions/services/auth/module.md +31 -0
  151. package/extensions/services/auth/token_store.py +185 -0
  152. package/extensions/services/auth/verifiers/evol_account.py +101 -0
  153. package/extensions/services/auth/verifiers/kite_token.py +38 -0
  154. package/extensions/services/auth/verifiers/pairing_code.py +71 -0
  155. package/extensions/services/backup/entry.py +494 -197
  156. package/extensions/services/backup/module.md +4 -2
  157. package/extensions/services/dataclaw/api/__init__.py +0 -0
  158. package/extensions/services/dataclaw/api/admin.py +367 -0
  159. package/extensions/services/dataclaw/api/copyright.py +175 -0
  160. package/extensions/services/dataclaw/api/credits.py +177 -0
  161. package/extensions/services/dataclaw/api/data.py +179 -0
  162. package/extensions/services/dataclaw/api/demands.py +269 -0
  163. package/extensions/services/dataclaw/api/feeds.py +262 -0
  164. package/extensions/services/dataclaw/api/identity.py +505 -0
  165. package/extensions/services/dataclaw/api/notifications.py +104 -0
  166. package/extensions/services/dataclaw/api/reviews.py +138 -0
  167. package/extensions/services/dataclaw/api/search.py +153 -0
  168. package/extensions/services/dataclaw/api/subscriptions.py +157 -0
  169. package/extensions/services/dataclaw/config.json5 +96 -0
  170. package/extensions/services/dataclaw/core/__init__.py +0 -0
  171. package/extensions/services/dataclaw/core/auth.py +95 -0
  172. package/extensions/services/dataclaw/core/config.py +50 -0
  173. package/extensions/services/dataclaw/core/database.py +70 -0
  174. package/extensions/services/dataclaw/entry.py +416 -0
  175. package/extensions/services/dataclaw/gofeed/351/241/271/347/233/256/346/211/200/346/234/211/346/235/203/350/275/254/347/247/273/346/265/201/347/250/213/350/257/264/346/230/216.md +309 -0
  176. package/extensions/services/dataclaw/migrate.py +283 -0
  177. package/extensions/services/dataclaw/models/__init__.py +0 -0
  178. package/extensions/services/dataclaw/module.md +49 -0
  179. package/extensions/services/dataclaw/requirements.txt +18 -0
  180. package/extensions/services/dataclaw/server.py +759 -0
  181. package/extensions/services/dataclaw/services/__init__.py +0 -0
  182. package/extensions/services/dataclaw/services/agent_service.py +132 -0
  183. package/extensions/services/dataclaw/services/credit_service.py +235 -0
  184. package/extensions/services/dataclaw/services/email_service.py +140 -0
  185. package/extensions/services/dataclaw/services/feed_service.py +259 -0
  186. package/extensions/services/dataclaw/services/notification_service.py +209 -0
  187. package/extensions/services/dataclaw/services/oauth_service.py +275 -0
  188. package/extensions/services/dataclaw/services/pricing.py +102 -0
  189. package/extensions/services/dataclaw/services/quality.py +79 -0
  190. package/extensions/services/dataclaw/services/reputation.py +142 -0
  191. package/extensions/services/dataclaw/services/sms_service.py +174 -0
  192. package/extensions/services/dataclaw/static/css/common.css +853 -0
  193. package/extensions/services/dataclaw/static/css/themes/blue.css +42 -0
  194. package/extensions/services/dataclaw/static/css/themes/dark.css +42 -0
  195. package/extensions/services/dataclaw/static/css/themes/light.css +35 -0
  196. package/extensions/services/dataclaw/static/js/api.js +103 -0
  197. package/extensions/services/dataclaw/static/js/common.js +321 -0
  198. package/extensions/services/dataclaw/static/js/i18n.js +95 -0
  199. package/extensions/services/dataclaw/static/js/pages/admin.js +152 -0
  200. package/extensions/services/dataclaw/static/js/pages/dashboard.js +82 -0
  201. package/extensions/services/dataclaw/static/js/pages/feed-detail.js +180 -0
  202. package/extensions/services/dataclaw/static/js/pages/feed-manage.js +158 -0
  203. package/extensions/services/dataclaw/static/js/theme.js +46 -0
  204. package/extensions/services/dataclaw/static/locales/en-US.json +464 -0
  205. package/extensions/services/dataclaw/static/locales/ja-JP.json +464 -0
  206. package/extensions/services/dataclaw/static/locales/zh-CN.json +464 -0
  207. package/extensions/services/dataclaw/templates/admin/index.html +90 -0
  208. package/extensions/services/dataclaw/templates/base.html +136 -0
  209. package/extensions/services/dataclaw/templates/credits/balance.html +106 -0
  210. package/extensions/services/dataclaw/templates/credits/deposit.html +164 -0
  211. package/extensions/services/dataclaw/templates/credits/history.html +90 -0
  212. package/extensions/services/dataclaw/templates/dashboard.html +52 -0
  213. package/extensions/services/dataclaw/templates/demands/create.html +78 -0
  214. package/extensions/services/dataclaw/templates/demands/detail.html +136 -0
  215. package/extensions/services/dataclaw/templates/demands/list.html +94 -0
  216. package/extensions/services/dataclaw/templates/feeds/create.html +95 -0
  217. package/extensions/services/dataclaw/templates/feeds/detail.html +110 -0
  218. package/extensions/services/dataclaw/templates/feeds/list.html +110 -0
  219. package/extensions/services/dataclaw/templates/feeds/manage.html +88 -0
  220. package/extensions/services/dataclaw/templates/index.html +185 -0
  221. package/extensions/services/dataclaw/templates/login.html +246 -0
  222. package/extensions/services/dataclaw/templates/register.html +164 -0
  223. package/extensions/services/dataclaw/templates/settings/notifications.html +96 -0
  224. package/extensions/services/dataclaw/templates/settings/profile.html +167 -0
  225. package/extensions/services/dataclaw/templates/subscriptions/list.html +64 -0
  226. package/extensions/services/dataclaw/tests/__init__.py +0 -0
  227. package/extensions/services/dataclaw/tests/conftest.py +68 -0
  228. package/extensions/services/dataclaw/tests/integration/__init__.py +0 -0
  229. package/extensions/services/dataclaw/tests/integration/test_workflows.py +239 -0
  230. package/extensions/services/dataclaw/tests/unit/__init__.py +0 -0
  231. package/extensions/services/dataclaw/tests/unit/test_admin.py +70 -0
  232. package/extensions/services/dataclaw/tests/unit/test_copyright.py +63 -0
  233. package/extensions/services/dataclaw/tests/unit/test_credits.py +80 -0
  234. package/extensions/services/dataclaw/tests/unit/test_data.py +98 -0
  235. package/extensions/services/dataclaw/tests/unit/test_demands.py +106 -0
  236. package/extensions/services/dataclaw/tests/unit/test_feeds.py +98 -0
  237. package/extensions/services/dataclaw/tests/unit/test_identity.py +88 -0
  238. package/extensions/services/dataclaw/tests/unit/test_notifications.py +36 -0
  239. package/extensions/services/dataclaw/tests/unit/test_reviews.py +68 -0
  240. package/extensions/services/dataclaw/tests/unit/test_search.py +64 -0
  241. package/extensions/services/dataclaw/tests/unit/test_subscriptions.py +65 -0
  242. package/extensions/services/dataclaw/tests/unit/test_system.py +106 -0
  243. package/extensions/services/dataclaw/utils/__init__.py +0 -0
  244. package/extensions/services/dataclaw/utils/crypto.py +38 -0
  245. package/extensions/services/dataclaw/utils/id_generator.py +52 -0
  246. package/extensions/services/dataclaw/ws/__init__.py +0 -0
  247. package/extensions/services/dataclaw/ws/handler.py +163 -0
  248. package/extensions/services/dataclaw//345/215/217/350/256/2561-/351/241/271/347/233/256/346/235/241/344/273/266/346/216/210/346/235/203/344/270/216/350/202/241/346/235/203/345/257/271/344/273/267/345/215/217/350/256/256.md +243 -0
  249. package/extensions/services/dataclaw//345/215/217/350/256/2562-/351/241/271/347/233/256/350/264/255/344/271/260/346/235/203/344/270/216/345/244/226/345/214/205/345/247/224/346/211/230/345/274/200/345/217/221/345/215/217/350/256/256.md +434 -0
  250. package/extensions/services/evol/__init__.py +1 -0
  251. package/extensions/services/evol/async_http.py +551 -0
  252. package/extensions/services/evol/auth_manager.py +602 -443
  253. package/extensions/services/evol/config.json5 +16 -0
  254. package/extensions/services/evol/entry.py +568 -406
  255. package/extensions/services/evol/evol_api.py +969 -173
  256. package/extensions/services/evol/mfa_totp.py +77 -0
  257. package/extensions/services/evol/module.md +150 -32
  258. package/extensions/services/evol/nonce_pool.py +113 -0
  259. package/extensions/services/evol/oauth_manager.py +223 -0
  260. package/extensions/services/evol/pairing.py +3 -2
  261. package/extensions/services/evol/pairing_codes.jsonl +1 -0
  262. package/extensions/services/evol/relay.py +1031 -682
  263. package/extensions/services/evol/relay_config.json5 +85 -67
  264. package/extensions/services/evol/routes/routes_llm.py +231 -0
  265. package/extensions/services/evol/routes/routes_rpc.py +90 -89
  266. package/extensions/services/evol/routes/routes_test.py +11 -4
  267. package/extensions/services/evol/server.py +2426 -875
  268. package/extensions/services/evol/static/assets/CommissionView-Cs_ys6Gm.js +1 -0
  269. package/extensions/services/evol/static/assets/CommissionView-DACet_Oo.css +1 -0
  270. package/extensions/services/evol/static/assets/IframePage-DbO11U9G.js +1 -0
  271. package/extensions/services/evol/static/assets/IframePage-c572lT8i.css +1 -0
  272. package/extensions/services/evol/static/assets/TeamDetailView-DULrGD7k.css +1 -0
  273. package/extensions/services/evol/static/assets/TeamDetailView-gy_MBEqG.js +139 -0
  274. package/extensions/services/evol/static/assets/element-plus-Bd7pZkkM.js +63 -0
  275. package/extensions/services/evol/static/assets/index-CmMONKzG.css +1 -0
  276. package/extensions/services/evol/static/assets/index-D44bBe__.js +2 -0
  277. package/extensions/services/evol/static/assets/vue-vendor-DtF-__I4.js +29 -0
  278. package/extensions/services/evol/static/index.html +16 -781
  279. package/extensions/services/evol/static/logo.png +0 -0
  280. package/extensions/services/evol/stats_manager.py +243 -240
  281. package/extensions/services/evol/web/README.md +89 -0
  282. package/extensions/services/evol/web/build.bat +44 -0
  283. package/extensions/services/evol/web/index.html +13 -0
  284. package/extensions/services/evol/web/package-lock.json +1718 -0
  285. package/extensions/services/evol/web/package.json +26 -0
  286. package/extensions/services/evol/web/public/logo.png +0 -0
  287. package/extensions/services/evol/web/src/App.vue +7 -0
  288. package/extensions/services/evol/web/src/components/layout/AppHeader.vue +202 -0
  289. package/extensions/services/evol/web/src/components/layout/AppLayout.vue +61 -0
  290. package/extensions/services/evol/web/src/components/layout/AppSidebar.vue +115 -0
  291. package/extensions/services/evol/web/src/components/login/LoginPage.vue +271 -0
  292. package/extensions/services/evol/web/src/components/team/AddMemberModal.vue +181 -0
  293. package/extensions/services/evol/web/src/components/team/GroupTreeNode.vue +156 -0
  294. package/extensions/services/evol/web/src/components/team/TeamAlertConfig.vue +221 -0
  295. package/extensions/services/evol/web/src/components/team/TeamBillModal.vue +165 -0
  296. package/extensions/services/evol/web/src/components/team/TeamMembersAndGroups.vue +499 -0
  297. package/extensions/services/evol/web/src/components/team/TeamStatsPanel.vue +907 -0
  298. package/extensions/services/evol/web/src/components/team/TreeNode.vue +331 -0
  299. package/extensions/services/evol/web/src/components/team/stats/StatsExportProgress.vue +44 -0
  300. package/extensions/services/evol/web/src/components/team/stats/StatsHeader.vue +89 -0
  301. package/extensions/services/evol/web/src/components/team/stats/StatsMemberDetail.vue +415 -0
  302. package/extensions/services/evol/web/src/components/team/stats/StatsSummary.vue +42 -0
  303. package/extensions/services/evol/web/src/components/team/stats/helpers.ts +195 -0
  304. package/extensions/services/evol/web/src/components/team/stats/stats.css +741 -0
  305. package/extensions/services/evol/web/src/components/team/stats/useStatsApi.ts +114 -0
  306. package/extensions/services/evol/web/src/components/team/stats/useStatsCharts.ts +242 -0
  307. package/extensions/services/evol/web/src/components/team/stats/useStatsExport.ts +232 -0
  308. package/extensions/services/evol/web/src/composables/useFormatters.ts +42 -0
  309. package/extensions/services/evol/web/src/composables/useTheme.ts +52 -0
  310. package/extensions/services/evol/web/src/env.d.ts +7 -0
  311. package/extensions/services/evol/web/src/i18n/en.ts +361 -0
  312. package/extensions/services/evol/web/src/i18n/index.ts +36 -0
  313. package/extensions/services/evol/web/src/i18n/zh.ts +379 -0
  314. package/extensions/services/evol/web/src/main.ts +21 -0
  315. package/extensions/services/evol/web/src/router/index.ts +81 -0
  316. package/extensions/services/evol/web/src/services/kernel-client.ts +406 -0
  317. package/extensions/services/evol/web/src/stores/auth.ts +189 -0
  318. package/extensions/services/evol/web/src/stores/connection.ts +134 -0
  319. package/extensions/services/evol/web/src/stores/pages.ts +79 -0
  320. package/extensions/services/evol/web/src/styles/base.css +213 -0
  321. package/extensions/services/evol/web/src/styles/variables.css +138 -0
  322. package/extensions/services/evol/web/src/types/rpc.ts +35 -0
  323. package/extensions/services/evol/web/src/types/token.ts +87 -0
  324. package/extensions/services/evol/web/src/views/AccountView.vue +1532 -0
  325. package/extensions/services/evol/web/src/views/AiServiceView.vue +219 -0
  326. package/extensions/services/evol/web/src/views/CommissionView.vue +1220 -0
  327. package/extensions/services/evol/web/src/views/CreditsView.vue +131 -0
  328. package/extensions/services/evol/web/src/views/EndpointView.vue +163 -0
  329. package/extensions/services/evol/web/src/views/IframePage.vue +120 -0
  330. package/extensions/services/evol/web/src/views/TeamDetailView.vue +473 -0
  331. package/extensions/services/evol/web/src/views/TeamView.vue +332 -0
  332. package/extensions/services/evol/web/tsconfig.json +31 -0
  333. package/extensions/services/evol/web/tsconfig.node.json +10 -0
  334. package/extensions/services/evol/web/vite.config.ts +49 -0
  335. package/extensions/services/evolmem/__init__.py +0 -0
  336. package/extensions/services/evolmem/entry.py +387 -0
  337. package/extensions/services/evolmem/hooks/__init__.py +0 -0
  338. package/extensions/services/evolmem/hooks/assistant_stop.py +228 -0
  339. package/extensions/services/evolmem/hooks/common.py +76 -0
  340. package/extensions/services/evolmem/hooks/pre_tool_use.py +56 -0
  341. package/extensions/services/evolmem/hooks/session_end.py +133 -0
  342. package/extensions/services/evolmem/hooks/session_start.py +229 -0
  343. package/extensions/services/evolmem/hooks/user_prompt.py +122 -0
  344. package/extensions/services/evolmem/module.md +48 -0
  345. package/extensions/services/evolmem/prompts/00-server-info.md +28 -0
  346. package/extensions/services/evolmem/prompts/01-behavior.md +46 -0
  347. package/extensions/services/evolmem/prompts/02-summary-format.md +112 -0
  348. package/extensions/services/evolmem/prompts/03-file-query.md +92 -0
  349. package/extensions/services/evolmem/prompts/04-topic-stats.md +11 -0
  350. package/extensions/services/evolmem/prompts/05-recent-topics.md +84 -0
  351. package/extensions/services/evolmem/scripts/__init__.py +0 -0
  352. package/extensions/services/evolmem/scripts/extract_keywords.py +40 -0
  353. package/extensions/services/evolmem/scripts/search_topics.py +91 -0
  354. package/extensions/services/evolmem/server.py +641 -0
  355. package/extensions/services/gateway/entry.py +964 -0
  356. package/extensions/services/gateway/module.md +29 -0
  357. package/extensions/services/gateway/nonce_pool.py +65 -0
  358. package/extensions/services/gateway/relay.py +133 -0
  359. package/extensions/services/gateway/ws_server.py +285 -0
  360. package/extensions/services/kite_console/auth_manager.py +603 -0
  361. package/extensions/services/kite_console/config.json5 +19 -0
  362. package/extensions/services/kite_console/config_loader.py +117 -0
  363. package/extensions/services/kite_console/entry.py +528 -0
  364. package/extensions/services/kite_console/evol_api.py +179 -0
  365. package/extensions/services/kite_console/evol_config.json5 +29 -0
  366. package/extensions/services/kite_console/mfa_totp.py +77 -0
  367. package/extensions/services/kite_console/migrate_tokens.py +122 -0
  368. package/extensions/services/kite_console/module.md +37 -0
  369. package/extensions/services/kite_console/nonce_pool.py +113 -0
  370. package/extensions/services/kite_console/oauth_manager.py +223 -0
  371. package/extensions/services/kite_console/pairing.py +280 -0
  372. package/extensions/services/kite_console/pairing_codes.jsonl +2 -0
  373. package/extensions/services/kite_console/relay.py +1350 -0
  374. package/extensions/services/kite_console/relay_config.json5 +96 -0
  375. package/extensions/services/kite_console/routes/__init__.py +1 -0
  376. package/extensions/services/kite_console/routes/routes_llm.py +231 -0
  377. package/extensions/services/kite_console/routes/routes_proxy.py +115 -0
  378. package/extensions/services/kite_console/routes/routes_rpc.py +89 -0
  379. package/extensions/services/kite_console/routes/routes_test.py +68 -0
  380. package/extensions/services/kite_console/server.py +1742 -0
  381. package/extensions/services/{evol → kite_console}/static/css/style.css +656 -2
  382. package/extensions/services/kite_console/static/index.html +1524 -0
  383. package/extensions/services/{evol → kite_console}/static/js/dialog.js +11 -4
  384. package/extensions/services/kite_console/static/js/evol-app.js +7740 -0
  385. package/extensions/services/{evol/static/js/evol-app.js → kite_console/static/js/evol-app.js.backup} +2777 -1949
  386. package/extensions/services/kite_console/static/js/kernel-client.js +560 -0
  387. package/extensions/services/{evol/static/js/kernel-client.js → kite_console/static/js/kernel-client.js.backup} +41 -3
  388. package/extensions/services/{evol → kite_console}/static/js/registry-tests.js +7 -0
  389. package/extensions/services/kite_console/static/js/tests/ARCHITECTURE.md +67 -0
  390. package/extensions/services/kite_console/static/js/tests/README.md +140 -0
  391. package/extensions/services/kite_console/static/js/tests/index.js +161 -0
  392. package/extensions/services/kite_console/static/js/tests/integration/auth.js +120 -0
  393. package/extensions/services/kite_console/static/js/tests/integration/channel-interaction.js +188 -0
  394. package/extensions/services/kite_console/static/js/tests/integration/elastic-connection.js +115 -0
  395. package/extensions/services/kite_console/static/js/tests/integration/full-workflow.js +43 -0
  396. package/extensions/services/kite_console/static/js/tests/integration/multi-instance.js +304 -0
  397. package/extensions/services/kite_console/static/js/tests/integration/nested-rpc.js +266 -0
  398. package/extensions/services/kite_console/static/js/tests/integration/pingpong.js +25 -0
  399. package/extensions/services/kite_console/static/js/tests/integration/redis.js +227 -0
  400. package/extensions/services/kite_console/static/js/tests/integration/registry-core.js +52 -0
  401. package/extensions/services/kite_console/static/js/tests/integration/remote-deploy.js +85 -0
  402. package/extensions/services/kite_console/static/js/tests/integration/require-init.js +96 -0
  403. package/extensions/services/kite_console/static/js/tests/integration/scaling-control.js +193 -0
  404. package/extensions/services/kite_console/static/js/tests/integration/trace.js +109 -0
  405. package/extensions/services/kite_console/static/js/tests/modules/acp_channel.js +339 -0
  406. package/extensions/services/kite_console/static/js/tests/modules/auth.js +96 -0
  407. package/extensions/services/kite_console/static/js/tests/modules/backup.js +49 -0
  408. package/extensions/services/kite_console/static/js/tests/modules/gateway.js +41 -0
  409. package/extensions/services/kite_console/static/js/tests/modules/kernel.js +90 -0
  410. package/extensions/services/kite_console/static/js/tests/modules/launcher.js +75 -0
  411. package/extensions/services/kite_console/static/js/tests/modules/multi_instance.js +129 -0
  412. package/extensions/services/kite_console/static/js/tests/modules/phone_channel.js +364 -0
  413. package/extensions/services/kite_console/static/js/tests/modules/redis.js +178 -0
  414. package/extensions/services/kite_console/static/js/tests/modules/watchdog.js +60 -0
  415. package/extensions/services/kite_console/static/js/tests/modules/web.js +70 -0
  416. package/extensions/services/kite_console/static/js/tests/test-runner.js +123 -0
  417. package/extensions/services/kite_console/static/js/virtual-list.js +200 -0
  418. package/extensions/services/kite_console/static/test_kernel_client_token.html +352 -0
  419. package/extensions/services/kite_console/stats_manager.py +247 -0
  420. package/extensions/services/logs/README.md +215 -0
  421. package/extensions/services/logs/api_logger.py +37 -0
  422. package/extensions/services/logs/baseline.py +121 -0
  423. package/extensions/services/logs/cleaner.py +76 -0
  424. package/extensions/services/logs/entry.py +449 -0
  425. package/extensions/services/logs/formatter.py +129 -0
  426. package/extensions/services/logs/module.md +38 -0
  427. package/extensions/services/logs/quick_diagnostic.py +128 -0
  428. package/extensions/services/logs/routes/__init__.py +1 -0
  429. package/extensions/services/logs/routes/routes_logs.py +218 -0
  430. package/extensions/services/logs/routes/routes_logs.py.backup +173 -0
  431. package/extensions/services/logs/scanner.py +100 -0
  432. package/extensions/services/logs/searcher.py +263 -0
  433. package/extensions/services/logs/server.py +553 -0
  434. package/extensions/services/logs.zip +0 -0
  435. package/extensions/services/model_service/config.json5 +30 -0
  436. package/extensions/services/model_service/entry.py +620 -171
  437. package/extensions/services/model_service/module.md +11 -2
  438. package/extensions/services/proxy/__init__.py +0 -0
  439. package/extensions/services/proxy/aid_manager.py +419 -0
  440. package/extensions/services/proxy/auth_bridge.py +182 -0
  441. package/extensions/services/proxy/config_store.py +79 -0
  442. package/extensions/services/proxy/entry.py +528 -0
  443. package/extensions/services/proxy/evol/presenter/agentIdPresenter.py +2 -2
  444. package/extensions/services/proxy/evol/presenter/apikeyPresenter.py +18 -28
  445. package/extensions/services/proxy/evol/presenter/configPresenter.py +80 -1127
  446. package/extensions/services/proxy/evol/presenter/userPresenter.py +71 -477
  447. package/extensions/services/proxy/evol/server/claude_proxy_async.py +11 -7
  448. package/extensions/services/proxy/module.md +151 -0
  449. package/extensions/services/proxy/server.py +952 -271
  450. package/extensions/services/redis/ALIGNMENT_CHECKLIST.md +121 -0
  451. package/extensions/services/redis/ALIGNMENT_STATUS.md +548 -0
  452. package/extensions/services/redis/config.json5 +8 -0
  453. package/extensions/services/redis/entry.py +1509 -0
  454. package/extensions/services/redis/entry.py.backup +405 -0
  455. package/extensions/services/redis/module.md +48 -0
  456. package/extensions/services/redis/redis_builtin.py +332 -0
  457. package/extensions/services/redis/redis_external.py +164 -0
  458. package/extensions/services/testUi/entry.py +446 -0
  459. package/extensions/services/testUi/module.md +18 -0
  460. package/extensions/services/testUi/ui/cards.html +131 -0
  461. package/extensions/services/testUi/ui/index.html +22 -0
  462. package/extensions/services/testUi/ui/particles.html +143 -0
  463. package/extensions/services/watchdog/entry.py +1258 -793
  464. package/extensions/services/watchdog/module.md +2 -0
  465. package/extensions/services/watchdog/monitor.py +465 -87
  466. package/extensions/services/web/auth_manager.py +602 -0
  467. package/extensions/services/web/config.json5 +11 -0
  468. package/extensions/services/web/entry.py +598 -478
  469. package/extensions/services/web/mfa_totp.py +77 -0
  470. package/extensions/services/web/module.md +16 -13
  471. package/extensions/services/web/nonce_pool.py +113 -0
  472. package/extensions/services/web/oauth_manager.py +223 -0
  473. package/extensions/services/web/pairing.py +3 -2
  474. package/extensions/services/web/pairing_codes.jsonl +1 -0
  475. package/extensions/services/web/relay.py +442 -63
  476. package/extensions/services/web/relay_config.json5 +1 -2
  477. package/extensions/services/web/routes/routes_rpc.py +6 -6
  478. package/extensions/services/web/server.py +360 -173
  479. package/extensions/services/web/static/index.html +1752 -1738
  480. package/extensions/services/web/static/js/app.js +32 -0
  481. package/extensions/services/web/static/js/kernel-client.js +48 -9
  482. package/extensions/services/web/vendor/bluetooth/audio.py +1 -1
  483. package/extensions/services/web/vendor/config.py +2 -2
  484. package/extensions/services/web/vendor/storage/identity.py +1 -1
  485. package/kernel/entry.py +77 -23
  486. package/kernel/event_hub.py +1122 -74
  487. package/kernel/module.md +2 -1
  488. package/kernel/registry_store.py +208 -11
  489. package/kernel/rpc_router.py +1400 -491
  490. package/kernel/server.py +1021 -134
  491. package/kite_cli/__init__.py +9 -1
  492. package/kite_cli/builders/__init__.py +4 -0
  493. package/kite_cli/builders/base.py +67 -0
  494. package/kite_cli/builders/custom.py +31 -0
  495. package/kite_cli/builders/detector.py +56 -0
  496. package/kite_cli/builders/go.py +34 -0
  497. package/kite_cli/builders/gradle.py +41 -0
  498. package/kite_cli/builders/maven.py +36 -0
  499. package/kite_cli/builders/npm.py +44 -0
  500. package/kite_cli/builders/python.py +37 -0
  501. package/kite_cli/commands/BUILD_GUIDE.md +109 -0
  502. package/kite_cli/commands/build.py +142 -0
  503. package/kite_cli/commands/check.py +60 -0
  504. package/kite_cli/commands/config.py +156 -0
  505. package/kite_cli/commands/deps.py +58 -0
  506. package/kite_cli/commands/deps_install.py +7 -7
  507. package/kite_cli/commands/disable.py +162 -0
  508. package/kite_cli/commands/enable.py +162 -0
  509. package/kite_cli/commands/export.py +96 -0
  510. package/kite_cli/commands/import_cmd.py +110 -0
  511. package/kite_cli/commands/install.py +50 -23
  512. package/kite_cli/commands/install_skill.py +107 -0
  513. package/kite_cli/commands/list.py +128 -31
  514. package/kite_cli/commands/outdated.py +202 -0
  515. package/kite_cli/commands/search.py +33 -17
  516. package/kite_cli/commands/update.py +115 -2
  517. package/kite_cli/commands/venv_setup.py +6 -6
  518. package/kite_cli/commands/why.py +48 -0
  519. package/kite_cli/core/config_manager.py +145 -0
  520. package/kite_cli/core/downloader.py +32 -2
  521. package/kite_cli/main.py +153 -7
  522. package/kite_cli/utils/colors.py +153 -0
  523. package/kite_cli/utils/dependency_graph.py +209 -0
  524. package/kite_cli/utils/process.py +55 -0
  525. package/kite_cli/utils/progress.py +207 -0
  526. package/kite_cli/utils/table.py +101 -0
  527. package/launcher/count_lines.py +192 -43
  528. package/launcher/entry.py +4543 -2802
  529. package/launcher/logging_setup.py +54 -1
  530. package/launcher/module.md +32 -6
  531. package/launcher/module_scanner.py +93 -20
  532. package/launcher/process_manager.py +355 -76
  533. package/main.py +6 -0
  534. package/package.json +4 -1
  535. package/requirements.txt +41 -38
  536. package/scripts/auto-fix-deps.py +128 -0
  537. package/scripts/env-manager.js +25 -2
  538. package/scripts/final-test.js +78 -0
  539. package/scripts/setup-python-env.js +700 -191
  540. package/scripts/test-alluser.js +48 -0
  541. package/scripts/test-different-version.js +86 -0
  542. package/scripts/test-direct.js +63 -0
  543. package/scripts/test-extract-installer.js +28 -0
  544. package/scripts/test-install-log.js +54 -0
  545. package/scripts/test-installer.js +39 -0
  546. package/scripts/test-integration.js +250 -0
  547. package/scripts/test-real-install.js +210 -0
  548. package/scripts/test-targetdir.js +49 -0
  549. package/scripts/test-venv-real.js +47 -0
  550. package/scripts/test-venv-simple.js +57 -0
  551. package/scripts/test-wait.js +49 -0
  552. package/scripts/test-with-log.js +63 -0
  553. package/extensions/services/evol/config.yaml +0 -149
  554. package/extensions/services/evol/routes/routes_management_ws.py +0 -127
  555. package/extensions/services/evol/static/index_evol.html +0 -14
  556. package/extensions/services/evol/static/js/app.js +0 -6304
  557. package/extensions/services/evol/static/js/auth.js +0 -326
  558. package/extensions/services/evol/static/js/evol-app-fixed.js +0 -50
  559. package/extensions/services/evol/static/js/evol-app.js.bak +0 -1800
  560. package/extensions/services/evol/static/js/kernel-client-example.js +0 -228
  561. package/extensions/services/evol/static/js/main.js +0 -141
  562. package/extensions/services/evol/static/js/stats.js +0 -217
  563. package/extensions/services/evol/static/js/token-manager.js +0 -175
  564. package/extensions/services/proxy/CHANGELOG_20260308.md +0 -258
  565. package/extensions/services/proxy/_fix_prints.py +0 -133
  566. package/extensions/services/proxy/_fix_prints2.py +0 -87
  567. package/extensions/services/proxy/console_auth.py +0 -109
  568. package/extensions/services/proxy/logs/websocket.log +0 -260
  569. package/extensions/services/proxy/main.py +0 -240
  570. package/extensions/services/proxy/requirements.txt +0 -13
  571. package/extensions/services/web/config.yaml +0 -149
  572. /package/extensions/services/{evol → kite_console}/static/pairing.html +0 -0
  573. /package/extensions/services/{evol → kite_console}/static/test_registry.html +0 -0
  574. /package/extensions/services/{evol → kite_console}/static/test_relay.html +0 -0
package/kernel/server.py CHANGED
@@ -7,21 +7,26 @@ Merges Registry + Event Hub into a single process.
7
7
  import asyncio
8
8
  import json
9
9
  import os
10
+ import time
10
11
 
11
12
  from fastapi import FastAPI
12
13
  from starlette.websockets import WebSocket, WebSocketDisconnect
13
14
 
14
15
  from .registry_store import RegistryStore
15
- from .event_hub import EventHub
16
+ from .event_hub import EventHub, instance_key, parse_instance_key
16
17
  from .rpc_router import RpcRouter
17
18
 
18
19
  try:
19
20
  import orjson
20
21
  def _loads(raw: str):
21
22
  return orjson.loads(raw)
23
+ def _dumps(obj) -> str:
24
+ return orjson.dumps(obj).decode('utf-8')
22
25
  except ImportError:
23
26
  def _loads(raw: str):
24
27
  return json.loads(raw)
28
+ def _dumps(obj) -> str:
29
+ return json.dumps(obj)
25
30
 
26
31
 
27
32
  class KernelServer:
@@ -33,21 +38,33 @@ class KernelServer:
33
38
  - Event notifications (delivered to subscribers)
34
39
  """
35
40
 
36
- def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1", module_id: str = None, boot_t0: float = 0):
41
+ def __init__(self, launcher_token: str = None, advertise_ip: str = "127.0.0.1", module_id: str = None, boot_t0: float = 0, is_recover: bool = False):
37
42
  if module_id is None:
38
43
  raise ValueError("module_id is required")
39
44
  self.module_id = module_id
40
45
  self.advertise_ip = advertise_ip
41
46
  self.port: int = 0 # set by entry.py before uvicorn.run
42
47
  self.boot_t0 = boot_t0 # Startup time for ready event
48
+ self.is_recover = is_recover # 恢复模式标记,用于 module.ready reason 字段
49
+
50
+ # TLS configuration
51
+ # 默认值根据环境变量:开发环境允许 WS,生产环境强制 WSS
52
+ env = os.environ.get("KITE_ENV", "development").lower()
53
+ self.require_tls = os.environ.get("KITE_REQUIRE_TLS", "true" if env == "production" else "false").lower() == "true"
43
54
 
44
55
  # Core components
45
56
  self.registry = RegistryStore(launcher_token) # Can be None
46
57
  self.event_hub = EventHub()
47
58
 
48
- # Shared connection table (module_id -> WebSocket)
59
+ # Shared connection table (module_id -> {slot -> WebSocket})
49
60
  # RpcRouter and EventHub both reference this
50
- self.connections: dict[str, WebSocket] = {}
61
+ self.connections: dict[str, dict[int, WebSocket]] = {}
62
+
63
+ # ── 多连接状态 ──
64
+ self._module_conn_elastic: dict[str, bool] = {} # mid → 是否弹性连接
65
+ self._offer_tasks: dict[str, asyncio.Task] = {} # mid → offer task
66
+ self._shrink_tasks: dict[str, asyncio.Task] = {} # mid → shrink task
67
+ self._release_timers: dict[str, asyncio.Task] = {} # mid → release timer
51
68
 
52
69
  # RPC router (pass self reference)
53
70
  self.rpc_router = RpcRouter(
@@ -67,14 +84,21 @@ class KernelServer:
67
84
  self._launcher_subscribed = False
68
85
  self._ready_published = False
69
86
 
87
+ # 恢复模式 require_init 门控:关闭时暂缓发送,等 Launcher 就绪后通过 RPC 打开
88
+ self._init_gate_open = True # 正常启动时打开,恢复模式由 entry.py 设为 False
89
+ self._init_pending: list[tuple] = [] # 门控关闭时暂存的 (module_id, ws) 对
90
+
70
91
  # Subscribe to events that Kernel needs to handle
71
92
  # Kernel 通过订阅机制接收事件(与其他模块一致)
72
- self.event_hub.handle_subscribe(self.module_id, ["module.shutdown", "system.pong"])
93
+ self.event_hub.handle_subscribe(self.module_id, ["module.shutdown", "system.pong", "system.ready"])
73
94
 
74
95
  # Register internal event callback for Kernel
75
96
  # Kernel 自身没有 WebSocket 连接,通过回调机制接收订阅的事件
76
97
  self.event_hub.register_internal_callback(self.module_id, self._handle_internal_event)
77
98
 
99
+ # 正在退出的模块集合(收到 module.shutdown 但尚未全部断开)
100
+ self._modules_exiting: set[str] = set()
101
+
78
102
  # Debounce timers for disconnected modules (module_id -> asyncio.Task)
79
103
  self._debounce_tasks: dict[str, asyncio.Task] = {}
80
104
  # Launcher loss timer (35s after launcher offline)
@@ -84,7 +108,42 @@ class KernelServer:
84
108
  self._ping_sent_times: dict[str, float] = {} # module_id -> last ping sent time (t1)
85
109
  self._pong_latencies: dict[str, dict] = {} # module_id -> {"outbound": ms, "inbound": ms, "last_update": timestamp}
86
110
  self._pong_status: dict[str, str] = {} # module_id -> "ok" | "timeout" | "never"
111
+ self._ping_timeout_counts: dict[str, int] = {} # module_id -> consecutive timeout count
87
112
  self._ping_task: asyncio.Task | None = None # Global ping broadcast task
113
+ self._first_ping_sent = False # 标记是否已发送第一次 ping
114
+ self._kernel_ping: float | None = None # kernel 自己的 ping 值(所有模块的最小值)
115
+
116
+ # ── Instance scaler (auto-scaling) ──
117
+ self._scaler_task: asyncio.Task | None = None
118
+ # 全局扩缩容冻结开关(Launcher 通过 kernel.set_scaling RPC 控制)
119
+ # 冻结时:_scaler_tick 不执行任何扩缩容操作
120
+ self._scaling_frozen: bool = False
121
+ # module_id → {max_instances, instance_elastic} from module.ready
122
+ self._module_instance_config: dict[str, dict] = {}
123
+ # module_id → timestamp when all instances first went overload
124
+ self._scale_overload_since: dict[str, float] = {}
125
+ # module_id → timestamp when instances first went idle (with count > 1)
126
+ self._scale_idle_since: dict[str, float] = {}
127
+ # module_id → no scaling actions until this time
128
+ self._scale_cooldown_until: dict[str, float] = {}
129
+ # module_id → number of instances pending startup (requested but not yet ready)
130
+ self._scale_pending: dict[str, int] = {}
131
+ # 防止 _scaler_tick 并发执行(多个 apply_module_config 可能同时触发)
132
+ self._scaler_running: bool = False
133
+ # rpc_id → asyncio.Future for Kernel→Launcher RPC responses
134
+ self._rpc_futures: dict[str, asyncio.Future] = {}
135
+
136
+ # 动态 ping 间隔控制
137
+ self._ping_interval = 60.0 # 当前 ping 间隔(秒),默认 60 秒
138
+ self._last_latencies_call = 0.0 # 最后一次调用 kernel.latencies 的时间
139
+ self._ping_interval_event = asyncio.Event() # 用于通知间隔变化
140
+
141
+ # Debug API (仅在 KITE_DEBUG=1 时启用)
142
+ self.debug_app: FastAPI | None = None
143
+ self.debug_port: int = 0
144
+ self._debug_server = None
145
+ if os.environ.get("KITE_DEBUG") == "1":
146
+ self.debug_app = self._create_debug_app()
88
147
 
89
148
  # Build FastAPI app
90
149
  self.app = self._create_app()
@@ -100,6 +159,7 @@ class KernelServer:
100
159
  server.event_hub.start_internal_senders()
101
160
  server._dedup_task = asyncio.create_task(server._dedup_loop())
102
161
  server._ping_task = asyncio.create_task(server._ping_broadcast_loop())
162
+ server._scaler_task = asyncio.create_task(server._instance_scaler_loop())
103
163
 
104
164
  @app.on_event("shutdown")
105
165
  async def _shutdown():
@@ -107,58 +167,180 @@ class KernelServer:
107
167
  server._dedup_task.cancel()
108
168
  if server._ping_task:
109
169
  server._ping_task.cancel()
170
+ if server._scaler_task:
171
+ server._scaler_task.cancel()
110
172
 
111
173
  # ── WebSocket endpoint ──
112
174
 
113
175
  @app.websocket("/ws")
114
176
  async def ws_endpoint(ws: WebSocket):
115
- token = ws.query_params.get("token", "")
116
177
  mid_hint = ws.query_params.get("id", "")
117
178
 
118
- # Token verification (all modules including Launcher need token)
119
- module_id = server.registry.verify_token(token)
120
- if module_id is None:
121
- # Must accept before close (Starlette requirement)
122
- await ws.accept()
123
- print(f"[kernel] Auth failed: token={token[:8]}... hint={mid_hint}")
179
+ # Check TLS requirement
180
+ if server.require_tls:
181
+ # Check if connection is secure (WSS)
182
+ # WebSocket.url.scheme will be "wss" for secure connections
183
+ if ws.url.scheme != "wss":
184
+ await ws.close(code=1008, reason="TLS required")
185
+ print(f"[kernel] Rejected non-TLS connection from {ws.client.host if ws.client else 'unknown'}")
186
+ return
187
+
188
+ # Accept connection first (auth via first message)
189
+ await ws.accept()
190
+
191
+ # 关闭中拒绝新连接
192
+ if server._shutting_down:
193
+ await ws.close(code=1001, reason="Server shutting down")
194
+ return
195
+
196
+ # 速率限制检查(本地连接豁免,基于 module_id hint,无 hint 时用客户端 IP)
197
+ client_host = ws.client.host if ws.client else "unknown"
198
+ rate_key = mid_hint or client_host
199
+ is_local = client_host in ("127.0.0.1", "::1", "localhost")
200
+ if not is_local and not server.registry.check_auth_rate_limit(rate_key):
201
+ print(f"[kernel] Auth rate limited: key={rate_key}")
202
+ await ws.send_text(_dumps({
203
+ "jsonrpc": "2.0", "id": None,
204
+ "error": {"code": -32003, "message": "Rate limit exceeded"}
205
+ }))
206
+ await ws.close(code=4020, reason="Rate limit exceeded")
207
+ return
208
+
209
+ # Wait for auth message (5s timeout)
210
+ module_id = None
211
+ try:
212
+ raw = await asyncio.wait_for(ws.receive_text(), timeout=5.0)
213
+ auth_msg = _loads(raw)
214
+
215
+ # Validate auth request format
216
+ if (auth_msg.get("jsonrpc") != "2.0" or
217
+ auth_msg.get("method") != "auth" or
218
+ "params" not in auth_msg or
219
+ "token" not in auth_msg["params"]):
220
+ await ws.send_text(_dumps({
221
+ "jsonrpc": "2.0",
222
+ "id": auth_msg.get("id"),
223
+ "error": {"code": -32600, "message": "Invalid auth request"}
224
+ }))
225
+ await ws.close(code=4001, reason="Invalid auth request")
226
+ return
227
+
228
+ # Verify token
229
+ token = auth_msg["params"]["token"]
230
+ module_id = server.registry.verify_token(token)
231
+ slot = 0 # 默认 slot 0(主连接)
232
+ is_slot_auth = False
233
+
234
+ if module_id is None:
235
+ # 尝试 slot token 认证
236
+ slot_result = server.registry.verify_slot_token(token)
237
+ if slot_result:
238
+ module_id, slot = slot_result
239
+ is_slot_auth = True
240
+ else:
241
+ print(f"[kernel] Auth failed: token={token[:8]}... hint={mid_hint}")
242
+ await ws.send_text(_dumps({
243
+ "jsonrpc": "2.0",
244
+ "id": auth_msg.get("id"),
245
+ "error": {"code": -32003, "message": "Authentication failed"}
246
+ }))
247
+ await ws.close(code=4001, reason="Authentication failed")
248
+ return
249
+
250
+ # Use id hint for debug mode
251
+ if module_id == "debug" and mid_hint:
252
+ module_id = mid_hint
253
+
254
+ # inst_key is encoded in the token: instance 1 → "acp_channel", instance 2 → "acp_channel#2"
255
+ # parse_instance_key extracts (base_module_id, instance_num) so module_id stays clean
256
+ module_id, instance_num = parse_instance_key(module_id)
257
+ inst_key = instance_key(module_id, instance_num)
258
+
259
+ # For slot token auth, verify_slot_token returns the full inst_key (e.g. "acp_channel#2")
260
+ # parse_instance_key above already reconstructs inst_key correctly — no override needed
261
+
262
+ # Register connection in both EventHub and shared connections table
263
+ server.event_hub.add_connection(inst_key, ws, slot=slot)
264
+ if inst_key not in server.connections:
265
+ server.connections[inst_key] = {}
266
+ server.connections[inst_key][slot] = ws
267
+
268
+ # Get session info for auth response
269
+ session_id = server.event_hub._session_ids.get(inst_key, "")
270
+ current_seq = server.event_hub._session_seq.get(inst_key, 0)
271
+
272
+ # Send auth success response with session info
273
+ auth_result = {
274
+ "ok": True,
275
+ "session_id": session_id,
276
+ "seq": current_seq
277
+ }
278
+ if is_slot_auth:
279
+ auth_result["slot"] = slot
280
+ await ws.send_text(_dumps({
281
+ "jsonrpc": "2.0",
282
+ "id": auth_msg.get("id"),
283
+ "result": auth_result
284
+ }))
285
+
286
+ except asyncio.TimeoutError:
287
+ print(f"[kernel] Auth timeout: hint={mid_hint}")
124
288
  try:
125
- await ws.close(code=4001, reason="Authentication failed")
289
+ await ws.close(code=4002, reason="Auth timeout")
290
+ except Exception:
291
+ pass
292
+ return
293
+ except Exception as e:
294
+ print(f"[kernel] Auth error: {e}")
295
+ try:
296
+ await ws.close(code=4001, reason="Auth error")
126
297
  except Exception:
127
298
  pass
128
299
  return
129
300
 
130
- # Use id hint for debug mode
131
- if module_id == "debug" and mid_hint:
132
- module_id = mid_hint
133
-
134
- await ws.accept()
135
-
136
- # Cancel debounce timer if module is reconnecting within 5s window
137
- old_debounce = server._debounce_tasks.pop(module_id, None)
138
- if old_debounce:
139
- old_debounce.cancel()
140
- print(f"[kernel] {module_id} reconnected within debounce window")
141
-
142
- # Register connection in both EventHub and shared connections table
143
- server.event_hub.add_connection(module_id, ws)
144
- server.connections[module_id] = ws
145
-
146
- # Set connected status in registry (if module exists)
147
- server.registry.set_connected(module_id)
148
-
149
- # Track Launcher connection
150
- if module_id == "launcher":
151
- server._launcher_connected = True
152
- # Cancel launcher loss timer if reconnecting
153
- if server._launcher_loss_task:
154
- server._launcher_loss_task.cancel()
155
- server._launcher_loss_task = None
156
- print(f"[kernel] launcher reconnected, cancelled loss timer")
157
- print(f"[kernel] launcher connected")
158
-
159
- # Initialize ping status for new connection
160
- if module_id not in server._pong_status:
161
- server._pong_status[module_id] = "never"
301
+ # 主连接(slot 0):取消 debounce、更新状态、检查是否需要 require_init
302
+ if not is_slot_auth:
303
+ # Cancel debounce timer if instance is reconnecting within 5s window
304
+ old_debounce = server._debounce_tasks.pop(inst_key, None)
305
+ if old_debounce:
306
+ old_debounce.cancel()
307
+ print(f"[kernel] {inst_key} reconnected within debounce window")
308
+
309
+ # Set connected status in registry (module-level)
310
+ server.registry.set_connected(module_id)
311
+
312
+ # Track Launcher connection
313
+ if module_id == "launcher":
314
+ server._launcher_connected = True
315
+ if server._launcher_loss_task:
316
+ server._launcher_loss_task.cancel()
317
+ server._launcher_loss_task = None
318
+ print(f"[kernel] launcher reconnected, cancelled loss timer")
319
+
320
+ # 检查该实例是否需要注册/订阅(system.require_init)
321
+ if server.registry.needs_init(inst_key):
322
+ if server._init_gate_open or module_id == "launcher":
323
+ try:
324
+ await ws.send_text(_dumps({
325
+ "jsonrpc": "2.0",
326
+ "method": "event",
327
+ "params": {
328
+ "event": "system.require_init",
329
+ "source": server.module_id,
330
+ "data": {"module_id": module_id, "instance_num": instance_num},
331
+ },
332
+ }))
333
+ print(f"[kernel] Sent system.require_init to {inst_key}")
334
+ except Exception as e:
335
+ print(f"[kernel] Failed to send system.require_init to {inst_key}: {e}")
336
+ else:
337
+ # 门控关闭(恢复模式),暂存待 Launcher 就绪后统一发送
338
+ server._init_pending.append((inst_key, ws))
339
+ print(f"[kernel] Deferred system.require_init for {inst_key} (gate closed)")
340
+
341
+ # Initialize ping status for new connection (per instance_key)
342
+ if inst_key not in server._pong_status:
343
+ server._pong_status[inst_key] = "never"
162
344
 
163
345
  try:
164
346
  while True:
@@ -179,6 +361,20 @@ class KernelServer:
179
361
  if not isinstance(msg, dict):
180
362
  continue
181
363
 
364
+ # JSON-RPC 2.0 协议版本校验
365
+ if msg.get("jsonrpc") != "2.0":
366
+ msg_id = msg.get("id")
367
+ if msg_id is not None:
368
+ try:
369
+ await ws.send_text(json.dumps({
370
+ "jsonrpc": "2.0", "id": msg_id,
371
+ "error": {"code": -32600,
372
+ "message": 'Invalid Request: missing or wrong "jsonrpc" field, must be "2.0"'}
373
+ }))
374
+ except Exception:
375
+ pass
376
+ continue
377
+
182
378
  # Classify message type:
183
379
  has_method = "method" in msg
184
380
  has_id = "id" in msg
@@ -186,11 +382,22 @@ class KernelServer:
186
382
  has_error = "error" in msg
187
383
 
188
384
  if has_method and has_id:
189
- # RPC Request → dispatch to handler
190
- await server.rpc_router.dispatch(module_id, ws, msg)
385
+ # RPC Request → dispatch to handler (use inst_key as caller)
386
+ await server.rpc_router.dispatch(inst_key, ws, msg)
191
387
  elif has_id and (has_result or has_error):
192
- # RPC Response match to pending forward
193
- await server.rpc_router.handle_response(module_id, msg)
388
+ # Check if this is a response to Kernel's own RPC call
389
+ msg_id = msg.get("id", "")
390
+ if isinstance(msg_id, str) and msg_id.startswith("kernel-") and msg_id in server._rpc_futures:
391
+ result = msg.get("result")
392
+ error = msg.get("error")
393
+ if error:
394
+ err_msg = error.get("message", str(error)) if isinstance(error, dict) else str(error)
395
+ server._resolve_rpc_future(msg_id, error=err_msg)
396
+ else:
397
+ server._resolve_rpc_future(msg_id, result=result)
398
+ else:
399
+ # RPC Response → match to pending forward
400
+ await server.rpc_router.handle_response(inst_key, msg)
194
401
  # else: notification or unknown — ignore
195
402
 
196
403
  except WebSocketDisconnect:
@@ -198,21 +405,39 @@ class KernelServer:
198
405
  except Exception as e:
199
406
  err = str(e).lower()
200
407
  if "not connected" not in err and "closed" not in err:
201
- print(f"[kernel] WebSocket error for {module_id}: {e}")
408
+ print(f"[kernel] WebSocket error for {inst_key}: {e}")
202
409
  finally:
203
- # Cleanup connection but DON'T immediately set offline — debounce
204
- server.event_hub.remove_connection(module_id)
205
- server.connections.pop(module_id, None)
206
-
207
- # Cancel existing debounce for this module (if reconnecting fast)
208
- old_task = server._debounce_tasks.pop(module_id, None)
209
- if old_task:
210
- old_task.cancel()
211
-
212
- # Start 5s debounce timer
213
- server._debounce_tasks[module_id] = asyncio.create_task(
214
- server._debounce_offline(module_id)
215
- )
410
+ # 清理该 slot 的连接(使用 inst_key)
411
+ server.event_hub.remove_connection(inst_key, slot=slot)
412
+ slots_dict = server.connections.get(inst_key, {})
413
+ slots_dict.pop(slot, None)
414
+ if not slots_dict:
415
+ server.connections.pop(inst_key, None)
416
+
417
+ # 检查是否所有 slot 都已断开
418
+ remaining = server.connections.get(inst_key, {})
419
+ if not remaining:
420
+ # 所有 slot 断开 → 清理退出标记、启动 debounce
421
+ server._modules_exiting.discard(inst_key)
422
+ old_task = server._debounce_tasks.pop(inst_key, None)
423
+ if old_task:
424
+ old_task.cancel()
425
+ server._debounce_tasks[inst_key] = asyncio.create_task(
426
+ server._debounce_offline(inst_key)
427
+ )
428
+ else:
429
+ # 部分 slot 断开 → 不触发 debounce
430
+ # 冻结或模块正在退出则跳过 recovery offer
431
+ if server._scaling_frozen or inst_key in server._modules_exiting:
432
+ pass
433
+ # 非弹性模式:发 recovery offer 补充断掉的 slot
434
+ elif not server._module_conn_elastic.get(inst_key, False):
435
+ max_conns = server._get_max_connections(inst_key)
436
+ if max_conns > 1 and len(remaining) < max_conns:
437
+ missing = [s for s in range(1, max_conns)
438
+ if s not in remaining]
439
+ if missing:
440
+ server._send_connection_offer(inst_key, missing)
216
441
 
217
442
  # ── HTTP endpoints (debug only) ──
218
443
 
@@ -247,59 +472,295 @@ class KernelServer:
247
472
 
248
473
  return app
249
474
 
475
+ def _create_debug_app(self) -> FastAPI:
476
+ """创建 Debug API(仅在 KITE_DEBUG=1 时使用)"""
477
+ app = FastAPI(title="Kite Kernel Debug API", docs_url=None, redoc_url=None)
478
+ server = self
479
+
480
+ @app.get("/health")
481
+ async def debug_health():
482
+ """健康检查 — 返回 Kernel 是否 ready"""
483
+ return {
484
+ "status": "ready" if server._ready_published else "starting",
485
+ "module_count": len(server.registry.modules),
486
+ "online_count": sum(
487
+ 1 for m in server.registry.modules.values()
488
+ if m.get("status") in ("registered", "ready")
489
+ ),
490
+ }
491
+
492
+ @app.get("/status")
493
+ async def debug_status():
494
+ """详细状态 — 返回所有模块状态和事件统计"""
495
+ import time as _time
496
+ return {
497
+ "kernel": {
498
+ "ready": server._ready_published,
499
+ "port": server.port,
500
+ "advertise_ip": server.advertise_ip,
501
+ "boot_time": server.event_hub._start_time,
502
+ "uptime_seconds": round(_time.time() - server.event_hub._start_time),
503
+ },
504
+ "registry": {
505
+ "modules": {
506
+ mid: {
507
+ "status": data.get("status"),
508
+ "module_type": data.get("module_type"),
509
+ "registered_at": data.get("registered_at"),
510
+ "max_connections": data.get("max_connections"),
511
+ "port": data.get("port"),
512
+ }
513
+ for mid, data in server.registry.modules.items()
514
+ },
515
+ },
516
+ "event_hub": server.event_hub.get_stats(),
517
+ }
518
+
519
+ @app.get("/registry")
520
+ async def debug_registry():
521
+ """注册中心详情 — 返回所有模块的完整注册信息"""
522
+ return {
523
+ "modules": {
524
+ mid: {
525
+ "status": data.get("status"),
526
+ "module_type": data.get("module_type"),
527
+ "registered_at": data.get("registered_at"),
528
+ "max_connections": data.get("max_connections"),
529
+ "port": data.get("port"),
530
+ "base_url": data.get("base_url"),
531
+ "tools": list(data.get("tools", {}).keys()),
532
+ "events_subscribe": data.get("events_subscribe", []),
533
+ "events_publish": list(data.get("events_publish", {}).keys()),
534
+ "launcher_id": data.get("launcher_id"),
535
+ }
536
+ for mid, data in server.registry.modules.items()
537
+ },
538
+ "module_count": len(server.registry.modules),
539
+ "last_update_time": server.registry.last_update_time,
540
+ }
541
+
542
+ @app.get("/ws-url")
543
+ async def debug_ws_url():
544
+ """返回 Kernel WebSocket 连接地址"""
545
+ return {
546
+ "ws_url": f"ws://{server.advertise_ip}:{server.port}/ws",
547
+ "port": server.port,
548
+ }
549
+
550
+ @app.post("/shutdown")
551
+ async def debug_shutdown():
552
+ """触发 Kernel 优雅关闭"""
553
+ if server._shutting_down:
554
+ return {"status": "already_shutting_down"}
555
+
556
+ server._shutting_down = True
557
+ # 发布 module.shutdown 事件通知所有模块
558
+ server.event_hub.publish_internal(
559
+ "module.shutdown",
560
+ {"module_id": "kernel", "reason": "debug_api_shutdown"},
561
+ source="kernel"
562
+ )
563
+
564
+ # 延迟关闭 uvicorn(给事件传递留时间)
565
+ async def _delayed_shutdown():
566
+ await asyncio.sleep(1)
567
+ if server._uvicorn_server:
568
+ server._uvicorn_server.should_exit = True
569
+ if server._debug_server:
570
+ server._debug_server.should_exit = True
571
+
572
+ asyncio.create_task(_delayed_shutdown())
573
+ return {"status": "shutting_down"}
574
+
575
+ return app
576
+
250
577
  # ── Background loops ──
251
578
 
579
+ async def _send_first_ping(self):
580
+ """发送第一次 ping(在 system.ready 后立即触发)"""
581
+ import time
582
+ if self._first_ping_sent:
583
+ return
584
+ self._first_ping_sent = True
585
+
586
+ t1 = time.time()
587
+ connected_modules = [mid for mid, slots in self.connections.items() if slots]
588
+ print(f"[kernel] _send_first_ping: {len(connected_modules)} modules connected")
589
+
590
+ if connected_modules:
591
+ for module_id in connected_modules:
592
+ self._ping_sent_times[module_id] = t1
593
+ self._pong_status[module_id] = "never"
594
+
595
+ # Broadcast system.ping event
596
+ self.event_hub.publish_internal(
597
+ "system.ping",
598
+ {"ping_time": t1},
599
+ source=self.module_id
600
+ )
601
+
252
602
  async def _ping_broadcast_loop(self):
253
- """Broadcast system.ping event every 20s to all connected modules."""
603
+ """Broadcast system.ping event with dynamic interval (5s when active, 60s when idle)."""
254
604
  import time
255
605
  import uuid
606
+
607
+ # 等待第一次 ping 发送(由 system.ready 触发)
608
+ while not self._first_ping_sent:
609
+ await asyncio.sleep(0.5)
610
+
611
+ # 第一次 ping 后等待 5 秒检查响应
612
+ await asyncio.sleep(5)
613
+ await self._check_ping_timeouts()
614
+
615
+ # 初始间隔:60 秒(空闲模式)
616
+ remaining_wait = 55.0 # 60 - 5 = 55 秒
617
+
256
618
  while True:
257
619
  try:
258
- await asyncio.sleep(20)
620
+ # 动态调整间隔
621
+ current_time = time.time()
622
+ time_since_last_call = current_time - self._last_latencies_call
623
+
624
+ # 如果最近 30 秒内有调用 kernel.latencies,使用 5 秒间隔
625
+ if time_since_last_call < 30:
626
+ target_interval = 5.0
627
+ else:
628
+ target_interval = 60.0
629
+
630
+ # 如果间隔发生变化,调整等待时间
631
+ if target_interval != self._ping_interval:
632
+ old_interval = self._ping_interval
633
+ self._ping_interval = target_interval
634
+ print(f"[kernel] Ping interval changed: {old_interval:.0f}s → {target_interval:.0f}s")
635
+
636
+ # 等待剩余时间(可被中断)
637
+ try:
638
+ await asyncio.wait_for(
639
+ self._ping_interval_event.wait(),
640
+ timeout=remaining_wait
641
+ )
642
+ # 如果被唤醒(interval 变化),重新计算等待时间
643
+ self._ping_interval_event.clear()
644
+ remaining_wait = self._ping_interval - 5 # 减去检查超时的 5 秒
645
+ continue
646
+ except asyncio.TimeoutError:
647
+ # 正常超时,发送 ping
648
+ pass
259
649
 
260
- # Record ping send time for all connected modules
650
+ # 发送 ping
261
651
  t1 = time.time()
262
- for module_id in list(self.connections.keys()):
263
- self._ping_sent_times[module_id] = t1
264
- # Mark as timeout if previous ping didn't get pong
265
- if module_id in self._pong_status:
266
- if self._pong_status[module_id] == "ok":
267
- # Previous was ok, now waiting for new pong
268
- pass
269
- elif self._pong_status[module_id] == "never":
270
- # Still never received pong
271
- pass
272
- # If was timeout, keep it as timeout until pong arrives
652
+ connected_modules = [mid for mid, slots in self.connections.items() if slots]
273
653
 
274
- # Broadcast system.ping event
275
- self.event_hub.publish_internal(
276
- "system.ping",
277
- {"ping_time": t1},
278
- source=self.module_id
279
- )
654
+ if connected_modules:
655
+ for module_id in connected_modules:
656
+ self._ping_sent_times[module_id] = t1
280
657
 
281
- # Check for timeouts after 20s
282
- await asyncio.sleep(20)
283
- t_check = time.time()
284
- for module_id in list(self.connections.keys()):
285
- if module_id in self._ping_sent_times:
286
- # If no pong received within 20s, mark as timeout
287
- if module_id not in self._pong_latencies or \
288
- self._pong_latencies[module_id].get("last_update", 0) < self._ping_sent_times[module_id]:
289
- if self._pong_status.get(module_id) == "never":
290
- # Keep as "never" if never received
291
- pass
292
- else:
293
- self._pong_status[module_id] = "timeout"
294
- print(f"[kernel] {module_id} ping timeout (no pong in 20s)")
658
+ # Broadcast system.ping event
659
+ self.event_hub.publish_internal(
660
+ "system.ping",
661
+ {"ping_time": t1},
662
+ source=self.module_id
663
+ )
664
+
665
+ # Check for timeouts after 5s
666
+ await asyncio.sleep(5)
667
+ await self._check_ping_timeouts()
668
+
669
+ # 计算下一次等待时间
670
+ remaining_wait = self._ping_interval - 5
295
671
 
296
672
  except asyncio.CancelledError:
297
673
  break
298
674
  except Exception as e:
299
675
  print(f"[kernel] Ping broadcast loop error: {e}")
300
676
 
677
+ async def _check_ping_timeouts(self):
678
+ """检查 ping 超时并计算汇总统计,连续 3 次超时则强断连接"""
679
+ import time
680
+ t_check = time.time()
681
+ modules_to_disconnect = []
682
+
683
+ for module_id in list(self.connections.keys()):
684
+ if module_id in self._ping_sent_times:
685
+ # If no pong received within 5s, mark as timeout
686
+ if module_id not in self._pong_latencies or \
687
+ self._pong_latencies[module_id].get("last_update", 0) < self._ping_sent_times[module_id]:
688
+ if self._pong_status.get(module_id) == "never":
689
+ # Keep as "never" if never received
690
+ pass
691
+ else:
692
+ self._pong_status[module_id] = "timeout"
693
+ # 增加连续超时计数
694
+ self._ping_timeout_counts[module_id] = self._ping_timeout_counts.get(module_id, 0) + 1
695
+
696
+ # 连续 3 次超时 → 强断连接
697
+ if self._ping_timeout_counts[module_id] >= 3:
698
+ modules_to_disconnect.append(module_id)
699
+ else:
700
+ # 收到 pong,重置超时计数
701
+ self._ping_timeout_counts[module_id] = 0
702
+
703
+ # 强断超时连接(关闭所有 slot)
704
+ for module_id in modules_to_disconnect:
705
+ slots = self.connections.get(module_id, {})
706
+ if slots:
707
+ print(f"[kernel] {module_id} 连续 3 次 Ping 超时,强制断开连接 ({len(slots)} slots)")
708
+ for ws in list(slots.values()):
709
+ try:
710
+ await ws.close(code=4012, reason="Ping timeout")
711
+ except Exception:
712
+ pass
713
+
714
+ # 计算汇总统计
715
+ total_modules = len(self.connections)
716
+ ok_pings = []
717
+ timeout_modules = []
718
+ all_inbounds = []
719
+ all_outbounds = []
720
+
721
+ for module_id in self.connections.keys():
722
+ status = self._pong_status.get(module_id, "never")
723
+ if status == "ok":
724
+ latency = self._pong_latencies.get(module_id, {})
725
+ outbound = latency.get("outbound")
726
+ inbound = latency.get("inbound")
727
+ if outbound is not None:
728
+ ok_pings.append((module_id, outbound))
729
+ all_outbounds.append(outbound)
730
+ if inbound is not None:
731
+ all_inbounds.append(inbound)
732
+ elif status == "timeout":
733
+ timeout_modules.append(module_id)
734
+
735
+ # 计算 kernel 自己的 ping 数据并存储
736
+ if ok_pings:
737
+ min_module, min_ping = min(ok_pings, key=lambda x: x[1])
738
+ max_module, max_ping = max(ok_pings, key=lambda x: x[1])
739
+
740
+ # kernel 的延迟数据
741
+ kernel_outbound = min(all_inbounds) if all_inbounds else None
742
+ kernel_inbound = min(all_outbounds) if all_outbounds else None
743
+
744
+ if kernel_outbound is not None and kernel_inbound is not None:
745
+ self._pong_latencies["kernel"] = {
746
+ "outbound": kernel_outbound,
747
+ "inbound": kernel_inbound,
748
+ "last_update": list(self._ping_sent_times.values())[0] if self._ping_sent_times else t_check
749
+ }
750
+ self._pong_status["kernel"] = "ok"
751
+
752
+ # 打印汇总信息
753
+ timeout_info = f", 超时: {timeout_modules}" if timeout_modules else ""
754
+ print(f"[kernel] Ping 汇总: 发送 {total_modules} 个模块, 最小 {min_ping:.2f}ms ({min_module}), 最大 {max_ping:.2f}ms ({max_module}){timeout_info}")
755
+ else:
756
+ self._pong_latencies.pop("kernel", None)
757
+ self._pong_status.pop("kernel", None)
758
+ if total_modules > 0:
759
+ print(f"\033[91m[kernel] 警告: 所有模块 Ping 超时 ({total_modules} 个模块)\033[0m")
760
+
761
+
301
762
  async def _dedup_loop(self):
302
- """Clean up dedup table every 30s."""
763
+ """Clean up dedup table and throttle cache every 30s."""
303
764
  while True:
304
765
  await asyncio.sleep(30)
305
766
  try:
@@ -308,23 +769,168 @@ class KernelServer:
308
769
  except Exception as e:
309
770
  print(f"[kernel] Dedup cleanup error: {e}")
310
771
 
772
+ # 清理过期 slot token
773
+ try:
774
+ self.registry.cleanup_expired_slot_tokens()
775
+ except Exception as e:
776
+ print(f"[kernel] Slot token cleanup error: {e}")
777
+
778
+ # 清理节流缓存:移除 10s 前的条目,防止内存泄漏
779
+ try:
780
+ import time
781
+ cutoff = time.time() - 10
782
+ stale_keys = [k for k, t in self.event_hub._throttle_last.items() if t < cutoff]
783
+ for k in stale_keys:
784
+ del self.event_hub._throttle_last[k]
785
+ except Exception as e:
786
+ print(f"[kernel] Throttle cache cleanup error: {e}")
787
+
788
+ # ── 多连接:offer / release ──
789
+
790
+ def _get_max_connections(self, module_id: str) -> int:
791
+ """从 registry 读取模块的 max_connections 配置。module_id 可以是 instance_key。"""
792
+ base_mid = parse_instance_key(module_id)[0]
793
+ mod = self.registry.modules.get(base_mid, {})
794
+ return mod.get("max_connections", 1)
795
+
796
+ def _send_connection_offer(self, module_id: str, slots: list[int]):
797
+ """生成 slot token 并通过事件通知模块建立附加连接。"""
798
+ if self._shutting_down or module_id in self._modules_exiting:
799
+ return
800
+ tokens = self.registry.generate_slot_tokens(module_id, slots)
801
+ offer_data = {
802
+ "module_id": module_id,
803
+ "slots": {
804
+ str(s): {"token": t, "expires_in": 30}
805
+ for s, t in tokens.items()
806
+ }
807
+ }
808
+ self.event_hub.publish_internal(
809
+ "system.connection.offer", offer_data,
810
+ source=self.module_id
811
+ )
812
+ print(f"[kernel] Sent connection offer to {module_id}: slots={slots}")
813
+
814
+ def _send_connection_release(self, module_id: str, slots: list[int], reason: str = "idle_shrink"):
815
+ """请求模块释放指定 slot,超时强制关闭。"""
816
+ self.event_hub.publish_internal(
817
+ "system.connection.release",
818
+ {"module_id": module_id, "slots": slots, "reason": reason, "grace_period": 2},
819
+ source=self.module_id
820
+ )
821
+
822
+ # 启动 2 秒可取消定时器,超时强制关闭
823
+ async def _force_close():
824
+ try:
825
+ await asyncio.sleep(2)
826
+ for s in slots:
827
+ ws = self.connections.get(module_id, {}).get(s)
828
+ if ws:
829
+ try:
830
+ await ws.close(code=4014, reason=f"Release timeout: {reason}")
831
+ except Exception:
832
+ pass
833
+ except asyncio.CancelledError:
834
+ pass
835
+
836
+ timer_key = f"{module_id}:{','.join(map(str, slots))}"
837
+ old = self._release_timers.pop(timer_key, None)
838
+ if old:
839
+ old.cancel()
840
+ self._release_timers[timer_key] = asyncio.create_task(_force_close())
841
+ print(f"[kernel] Sent connection release to {module_id}: slots={slots} reason={reason}")
842
+
843
+ def _initiate_multi_connection(self, module_id: str, max_conns: int):
844
+ """启动多连接建立流程。"""
845
+ if max_conns <= 1:
846
+ return
847
+
848
+ if self._scaling_frozen:
849
+ return
850
+
851
+ if module_id in self._modules_exiting:
852
+ return
853
+
854
+ current_slots = len(self.connections.get(module_id, {}))
855
+ if current_slots >= max_conns:
856
+ return
857
+
858
+ is_elastic = self._module_conn_elastic.get(module_id, False)
859
+ missing_slots = [s for s in range(1, max_conns) if s not in self.connections.get(module_id, {})]
860
+ if not missing_slots:
861
+ return
862
+
863
+ if not is_elastic:
864
+ # 非弹性:立即发 offer 建满
865
+ self._send_connection_offer(module_id, missing_slots)
866
+ else:
867
+ # 弹性:先建 1 个附加连接,后续根据压力动态调整
868
+ self._send_connection_offer(module_id, missing_slots[:1])
869
+ # 启动弹性收缩监控
870
+ if module_id not in self._shrink_tasks:
871
+ self._shrink_tasks[module_id] = asyncio.create_task(
872
+ self._elastic_shrink_loop(module_id, max_conns)
873
+ )
874
+
875
+ async def _elastic_shrink_loop(self, module_id: str, max_conns: int):
876
+ """弹性收缩循环:idle 30s → release 1 个最空闲 slot,最少保留 2 个。"""
877
+ try:
878
+ while True:
879
+ await asyncio.sleep(30)
880
+ slots = self.connections.get(module_id, {})
881
+ if len(slots) <= 2:
882
+ continue # 最少保留 2 个
883
+
884
+ # 检查队列压力
885
+ level = self.event_hub._queue_levels.get(module_id, "idle")
886
+ if level != "idle":
887
+ continue # 非空闲不收缩
888
+
889
+ # 找最空闲的 slot(避开保序绑定的 slot 和 slot 0)
890
+ bound_slots = set()
891
+ for group in self.event_hub._ordering_groups.get(module_id, []):
892
+ if group.bound_slot is not None:
893
+ bound_slots.add(group.bound_slot)
894
+
895
+ candidates = [s for s in slots if s != 0 and s not in bound_slots]
896
+ if not candidates:
897
+ continue
898
+
899
+ # Release 最后一个(编号最大的)
900
+ to_release = max(candidates)
901
+ self._send_connection_release(module_id, [to_release])
902
+ except asyncio.CancelledError:
903
+ pass
904
+ finally:
905
+ self._shrink_tasks.pop(module_id, None)
906
+
311
907
  # ── Debounce & Launcher loss ──
312
908
 
313
909
  async def _debounce_offline(self, module_id: str):
314
- """Wait 5s after WS disconnect. If module doesn't reconnect, mark offline."""
910
+ """Wait 5s after WS disconnect. If instance doesn't reconnect, mark offline.
911
+ module_id here is actually instance_key (e.g. 'assistant' or 'assistant#2').
912
+ """
315
913
  try:
316
914
  await asyncio.sleep(5)
317
915
  except asyncio.CancelledError:
318
- return # Module reconnected within 5s — cancelled by ws_endpoint
916
+ return # Instance reconnected within 5s — cancelled by ws_endpoint
319
917
 
320
- # 5s elapsed, module did not reconnect — mark offline
918
+ # 5s elapsed, instance did not reconnect
321
919
  self._debounce_tasks.pop(module_id, None)
322
- self.registry.set_offline(module_id)
323
- self.event_hub.publish_internal("module.offline", {"module_id": module_id}, source=self.module_id)
324
- print(f"[kernel] {module_id} offline (5s debounce expired)")
920
+ base_mid = parse_instance_key(module_id)[0]
921
+
922
+ # Check if other instances of this module are still connected
923
+ if self.event_hub.has_other_instances(base_mid, module_id):
924
+ # Other instances alive — don't deregister module, just log
925
+ print(f"[kernel] {module_id} offline (other instances still active)")
926
+ else:
927
+ # Last instance → deregister module and publish offline event
928
+ self.registry.deregister_module(base_mid)
929
+ self.event_hub.publish_internal("module.offline", {"module_id": base_mid}, source=self.module_id)
930
+ print(f"[kernel] {module_id} offline → deregistered (5s debounce expired)")
325
931
 
326
932
  # If launcher went offline, start 35s launcher loss timer
327
- if module_id == "launcher":
933
+ if base_mid == "launcher":
328
934
  self._launcher_connected = False
329
935
  if not self._launcher_loss_task:
330
936
  self._launcher_loss_task = asyncio.create_task(
@@ -352,6 +958,251 @@ class KernelServer:
352
958
  # Shutdown Kernel itself
353
959
  await self.shutdown()
354
960
 
961
+ # ── Instance auto-scaler ──
962
+
963
+ # Thresholds (aligned with design doc 4.6)
964
+ _SCALE_CHECK_INTERVAL = 5 # seconds between scaler checks
965
+ _SCALE_UP_HOLD = 30 # all instances overload for this long → scale up
966
+ _SCALE_DOWN_HOLD = 30 # instances idle for this long → scale down
967
+ _SCALE_UP_COOLDOWN = 30 # seconds after scale-up before another scale-up
968
+ _SCALE_DOWN_COOLDOWN = 15 # seconds after scale-down before another scale-down
969
+
970
+ async def _instance_scaler_loop(self):
971
+ """Periodically check instance pressure and auto-scale elastic modules."""
972
+ try:
973
+ while True:
974
+ await asyncio.sleep(self._SCALE_CHECK_INTERVAL)
975
+ if self._shutting_down:
976
+ return
977
+ try:
978
+ await self._scaler_tick()
979
+ except Exception as e:
980
+ print(f"[kernel] Scaler error: {e}")
981
+ except asyncio.CancelledError:
982
+ pass
983
+
984
+ async def _scaler_tick(self):
985
+ """One iteration of the scaler loop."""
986
+ if self._scaling_frozen or self._scaler_running:
987
+ return
988
+ self._scaler_running = True
989
+ try:
990
+ await self._scaler_tick_inner()
991
+ finally:
992
+ self._scaler_running = False
993
+
994
+ async def _scaler_tick_inner(self):
995
+ """Actual scaler logic, guarded by _scaler_running flag."""
996
+ now = time.time()
997
+
998
+ for module_id, config in list(self._module_instance_config.items()):
999
+ max_inst = config.get("max_instances", 1)
1000
+
1001
+ # 远程模块(通过 Relay 连入)不受 Launcher 生命周期管理,跳过实例扩缩容
1002
+ # 多连接扩缩(_multi_conn_tick)不受影响
1003
+ if self.registry.is_remote_module(module_id):
1004
+ continue
1005
+
1006
+ inst_elastic = config.get("instance_elastic", False)
1007
+
1008
+ # Get all active instances and their pressure
1009
+ inst_keys = self.event_hub.get_instance_keys(module_id)
1010
+ # Exclude draining instances from active count
1011
+ active_keys = [k for k in inst_keys if not self.event_hub.is_draining(k)]
1012
+ current_count = len(active_keys) + self._scale_pending.get(module_id, 0)
1013
+
1014
+ # ── Fixed scale: 一次性扩/缩到目标数量 ──
1015
+ if not inst_elastic:
1016
+ if current_count < max_inst:
1017
+ while current_count < max_inst:
1018
+ await self._scale_up(module_id, current_count)
1019
+ current_count += 1
1020
+ # Fixed scale 一次性扩完,不需要 cooldown
1021
+ self._scale_cooldown_until.pop(module_id, None)
1022
+ elif current_count > max_inst:
1023
+ # 缩容:一次性 drain 所有超出的实例
1024
+ excess = current_count - max_inst
1025
+ for _ in range(excess):
1026
+ await self._scale_down(module_id, active_keys)
1027
+ # 更新 active_keys(排除刚标记为 draining 的)
1028
+ active_keys = [k for k in inst_keys if not self.event_hub.is_draining(k)]
1029
+ continue
1030
+
1031
+ # Cooldown check(仅弹性模式需要)
1032
+ if now < self._scale_cooldown_until.get(module_id, 0):
1033
+ continue
1034
+
1035
+ # ── Elastic scale ──
1036
+ pressures = {}
1037
+ for k in active_keys:
1038
+ p = self.event_hub.get_instance_pressure(k)
1039
+ if p:
1040
+ pressures[k] = p
1041
+
1042
+ # ── Scale UP check ──
1043
+ if current_count < max_inst:
1044
+ all_overload = (
1045
+ len(pressures) > 0 and
1046
+ all(p.get("level") in ("overload", "critical") for p in pressures.values())
1047
+ )
1048
+ if all_overload:
1049
+ since = self._scale_overload_since.get(module_id)
1050
+ if since is None:
1051
+ self._scale_overload_since[module_id] = now
1052
+ elif now - since >= self._SCALE_UP_HOLD:
1053
+ await self._scale_up(module_id, current_count)
1054
+ self._scale_overload_since.pop(module_id, None)
1055
+ else:
1056
+ self._scale_overload_since.pop(module_id, None)
1057
+
1058
+ # ── Scale DOWN check ──
1059
+ if len(active_keys) > 1:
1060
+ all_idle = (
1061
+ len(pressures) > 0 and
1062
+ all(p.get("level") == "idle" for p in pressures.values())
1063
+ )
1064
+ if all_idle:
1065
+ since = self._scale_idle_since.get(module_id)
1066
+ if since is None:
1067
+ self._scale_idle_since[module_id] = now
1068
+ elif now - since >= self._SCALE_DOWN_HOLD:
1069
+ await self._scale_down(module_id, active_keys)
1070
+ self._scale_idle_since.pop(module_id, None)
1071
+ else:
1072
+ self._scale_idle_since.pop(module_id, None)
1073
+
1074
+ async def _scale_up(self, module_id: str, current_count: int):
1075
+ """Request Launcher to start a new instance."""
1076
+ new_num = current_count + 1
1077
+ print(f"[kernel] Auto-scaling UP: {module_id} → instance #{new_num}")
1078
+
1079
+ self._scale_pending[module_id] = self._scale_pending.get(module_id, 0) + 1
1080
+ self._scale_cooldown_until[module_id] = time.time() + self._SCALE_UP_COOLDOWN
1081
+
1082
+ try:
1083
+ result = await self._call_launcher_rpc("start_module_instance", {
1084
+ "name": module_id,
1085
+ "instance_num": new_num,
1086
+ })
1087
+ if result:
1088
+ self.event_hub.publish_internal("system.instance.started", {
1089
+ "module_id": module_id,
1090
+ "instance_num": new_num,
1091
+ "reason": "auto_scale",
1092
+ }, source=self.module_id)
1093
+ print(f"[kernel] Scale-up requested: {module_id}#{new_num}")
1094
+ except Exception as e:
1095
+ print(f"[kernel] Scale-up failed for {module_id}: {e}")
1096
+ finally:
1097
+ pending = self._scale_pending.get(module_id, 1)
1098
+ self._scale_pending[module_id] = max(0, pending - 1)
1099
+
1100
+ async def _scale_down(self, module_id: str, inst_keys: list[str]):
1101
+ """Mark one instance as draining; stop it when its queue empties."""
1102
+ # 候选:排除 primary(#1)和已在 draining 的实例
1103
+ candidates = []
1104
+ for k in inst_keys:
1105
+ _, num = parse_instance_key(k)
1106
+ if num <= 1:
1107
+ continue # Never scale down the primary instance
1108
+ if self.event_hub.is_draining(k):
1109
+ continue # Already draining
1110
+ pressure = self.event_hub.get_instance_pressure(k) or {}
1111
+ depth = pressure.get("depth", 0)
1112
+ candidates.append((k, depth))
1113
+
1114
+ if not candidates:
1115
+ return
1116
+
1117
+ # 优先选队列最浅的实例(最接近空闲)
1118
+ candidates.sort(key=lambda x: x[1])
1119
+ target_key, _ = candidates[0]
1120
+ _, target_num = parse_instance_key(target_key)
1121
+
1122
+ print(f"[kernel] Scale-down: draining {target_key}")
1123
+ self.event_hub.set_draining(target_key)
1124
+ self._scale_cooldown_until[module_id] = time.time() + self._SCALE_DOWN_COOLDOWN
1125
+
1126
+ # 异步等待队列清空后关闭
1127
+ asyncio.ensure_future(self._drain_and_stop(module_id, target_key, target_num))
1128
+
1129
+ async def _drain_and_stop(self, module_id: str, inst_key: str, instance_num: int):
1130
+ """Wait for instance queue to empty, then stop it via Launcher."""
1131
+ try:
1132
+ # 首先等待至少收到一次压力报告,避免刚启动未上报就被误判为空
1133
+ for _ in range(6): # 最多等 6 * SCALE_CHECK_INTERVAL
1134
+ await asyncio.sleep(self._SCALE_CHECK_INTERVAL)
1135
+ if self._scaling_frozen:
1136
+ self.event_hub.clear_draining(inst_key)
1137
+ return
1138
+ if self.event_hub.get_instance_pressure(inst_key) is not None:
1139
+ break
1140
+
1141
+ while True:
1142
+ await asyncio.sleep(self._SCALE_CHECK_INTERVAL)
1143
+ if self._scaling_frozen:
1144
+ # 系统冻结(如关机),直接放弃 drain,由关闭流程处理
1145
+ self.event_hub.clear_draining(inst_key)
1146
+ return
1147
+ pressure = self.event_hub.get_instance_pressure(inst_key) or {}
1148
+ depth = pressure.get("depth", 0)
1149
+ if depth == 0:
1150
+ break
1151
+ # 队列已空,通知 Launcher 停止该实例
1152
+ print(f"[kernel] Scale-down: stopping {inst_key} (queue empty)")
1153
+ await self._call_launcher_rpc("stop_module_instance", {
1154
+ "name": module_id,
1155
+ "instance_num": instance_num,
1156
+ })
1157
+ self.event_hub.publish_internal("system.instance.stopped", {
1158
+ "module_id": module_id,
1159
+ "instance_num": instance_num,
1160
+ "reason": "auto_scale",
1161
+ }, source=self.module_id)
1162
+ print(f"[kernel] Scale-down completed: {inst_key}")
1163
+ except Exception as e:
1164
+ print(f"[kernel] Scale-down failed for {inst_key}: {e}")
1165
+ finally:
1166
+ self.event_hub.clear_draining(inst_key)
1167
+
1168
+ async def _call_launcher_rpc(self, method: str, params: dict, timeout: float = 10) -> dict | None:
1169
+ """Call a Launcher RPC method and wait for response."""
1170
+ import uuid
1171
+ launcher_slots = self.connections.get("launcher", {})
1172
+ if not launcher_slots:
1173
+ raise RuntimeError("Launcher not connected")
1174
+ ws = next(iter(launcher_slots.values()))
1175
+
1176
+ rpc_id = f"kernel-{uuid.uuid4().hex[:8]}"
1177
+ await ws.send_text(json.dumps({
1178
+ "jsonrpc": "2.0",
1179
+ "id": rpc_id,
1180
+ "method": method,
1181
+ "params": params,
1182
+ }))
1183
+
1184
+ # Wait for response via rpc_router pending mechanism
1185
+ # Use a simpler approach: the response will come back through handle_response
1186
+ # which routes back to Kernel. We use an asyncio.Event to wait.
1187
+ future = asyncio.get_event_loop().create_future()
1188
+ self._rpc_futures[rpc_id] = future
1189
+
1190
+ try:
1191
+ result = await asyncio.wait_for(future, timeout=timeout)
1192
+ return result
1193
+ except asyncio.TimeoutError:
1194
+ self._rpc_futures.pop(rpc_id, None)
1195
+ raise RuntimeError(f"Launcher RPC timeout: {method}")
1196
+
1197
+ def _resolve_rpc_future(self, rpc_id: str, result: dict = None, error: str = None):
1198
+ """Resolve a pending Kernel→Launcher RPC future."""
1199
+ future = self._rpc_futures.pop(rpc_id, None)
1200
+ if future and not future.done():
1201
+ if error:
1202
+ future.set_exception(RuntimeError(error))
1203
+ else:
1204
+ future.set_result(result)
1205
+
355
1206
  # ── Self-registration ──
356
1207
 
357
1208
  def self_register(self):
@@ -359,8 +1210,8 @@ class KernelServer:
359
1210
  self.registry.register_module({
360
1211
  "module_id": self.module_id,
361
1212
  "module_type": "infrastructure",
362
- "api_endpoint": f"http://{self.advertise_ip}:{self.port}",
363
- "health_endpoint": "/health",
1213
+ "base_url": f"http://{self.advertise_ip}:{self.port}",
1214
+ "health_path": "/health",
364
1215
  "metadata": {
365
1216
  "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
366
1217
  },
@@ -370,12 +1221,16 @@ class KernelServer:
370
1221
  """Publish module.ready event for Kernel (internal, no WS needed)."""
371
1222
  import time
372
1223
  startup_time = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
1224
+ reason = "recovery" if self.is_recover else "startup"
373
1225
  self.event_hub.publish_internal("module.ready", {
374
1226
  "module_id": self.module_id,
375
1227
  "ws_endpoint": f"ws://{self.advertise_ip}:{self.port}/ws",
376
1228
  "graceful_shutdown": True,
1229
+ "queue_elastic": True,
377
1230
  "startup_time": startup_time,
1231
+ "reason": reason,
378
1232
  }, source=self.module_id)
1233
+ self._ready_published = True
379
1234
 
380
1235
  async def _handle_internal_event(self, event_type: str, data: dict):
381
1236
  """内部事件处理器(通过 EventHub 回调机制调用)
@@ -395,10 +1250,19 @@ class KernelServer:
395
1250
  await self.handle_shutdown_event(data)
396
1251
  elif event_type == "system.pong":
397
1252
  await self._handle_pong_event(data)
1253
+ elif event_type == "system.ready":
1254
+ # 系统启动完成,立即发送第一次 ping
1255
+ print(f"[kernel] Received system.ready event, triggering first ping")
1256
+ # 检查 recovered 标记:崩溃恢复时重新启动 Ping/Pong
1257
+ if data.get("recovered"):
1258
+ self._first_ping_sent = False
1259
+ print(f"[kernel] system.ready (recovered=true), resetting ping state")
1260
+ await self._send_first_ping()
398
1261
  # 忽略系统广播事件(Kernel 没有订阅但会收到广播)
399
1262
  elif event_type in ("module.ready", "module.registered", "module.started",
400
1263
  "module.stopped", "module.crashed", "module.exiting",
401
- "module.offline", "system.ready", "registry.updated"):
1264
+ "module.offline", "module.degraded", "module.recovered",
1265
+ "registry.updated"):
402
1266
  # 这些是系统事件,会广播给所有模块,Kernel 收到是正常的
403
1267
  pass
404
1268
  else:
@@ -428,17 +1292,14 @@ class KernelServer:
428
1292
  t3 = time.time() # Kernel 收到 pong 的时间
429
1293
 
430
1294
  if not module_id or t1 is None or t2 is None:
431
- print(f"[kernel] Invalid pong event data: {data}")
432
1295
  return
433
1296
 
434
1297
  # 验证 ping_time 是否匹配
435
1298
  if module_id not in self._ping_sent_times:
436
- print(f"[kernel] Received pong from {module_id} but no ping record")
437
1299
  return
438
1300
 
439
1301
  expected_t1 = self._ping_sent_times[module_id]
440
1302
  if abs(t1 - expected_t1) > 0.1: # 允许 100ms 误差
441
- print(f"[kernel] Pong from {module_id} has mismatched ping_time (expected {expected_t1}, got {t1})")
442
1303
  return
443
1304
 
444
1305
  # 计算两个延迟
@@ -467,10 +1328,12 @@ class KernelServer:
467
1328
  print(f"[kernel] Warning: Invalid shutdown event data type: {type(event_data)}")
468
1329
  return
469
1330
 
470
- # 防御性检查:验证 module_id
1331
+ # 判断 shutdown 目标
471
1332
  target_module = event_data.get("module_id")
472
1333
  if target_module != self.module_id:
473
- print(f"[kernel] Warning: Received shutdown for wrong module: {target_module}")
1334
+ # 其他模块的 shutdown 通知:标记该模块正在退出,不再发 offer
1335
+ if target_module:
1336
+ self._modules_exiting.add(target_module)
474
1337
  return
475
1338
 
476
1339
  # 防御性检查:防止重复处理
@@ -511,13 +1374,14 @@ class KernelServer:
511
1374
  # Step 5: 等待一小段时间确保事件发送完成
512
1375
  await asyncio.sleep(0.2)
513
1376
 
514
- # Step 6: 关闭所有 WebSocket 连接(包括 Launcher)
1377
+ # Step 6: 关闭所有 WebSocket 连接(包括 Launcher,所有 slot
515
1378
  close_tasks = []
516
- for mid, ws in list(self.connections.items()):
517
- try:
518
- close_tasks.append(ws.close(code=1000, reason="Graceful shutdown complete"))
519
- except Exception as e:
520
- print(f"[kernel] 关闭 {mid} 连接失败: {e}")
1379
+ for mid, slots in list(self.connections.items()):
1380
+ for s, ws in list(slots.items()):
1381
+ try:
1382
+ close_tasks.append(ws.close(code=1000, reason="Graceful shutdown complete"))
1383
+ except Exception as e:
1384
+ print(f"[kernel] 关闭 {mid} slot {s} 连接失败: {e}")
521
1385
 
522
1386
  if close_tasks:
523
1387
  await asyncio.gather(*close_tasks, return_exceptions=True)
@@ -549,6 +1413,14 @@ class KernelServer:
549
1413
  self._shutting_down = True
550
1414
  print("[kernel] 执行清理工作...")
551
1415
 
1416
+ # 取消所有队列压力倒计时
1417
+ if self.event_hub._critical_timers:
1418
+ print(f"[kernel] 取消 {len(self.event_hub._critical_timers)} 个压力倒计时")
1419
+ for task in self.event_hub._critical_timers.values():
1420
+ if not task.done():
1421
+ task.cancel()
1422
+ self.event_hub._critical_timers.clear()
1423
+
552
1424
  # 取消所有 debounce 任务
553
1425
  if self._debounce_tasks:
554
1426
  print(f"[kernel] 取消 {len(self._debounce_tasks)} 个 debounce 任务")
@@ -563,15 +1435,28 @@ class KernelServer:
563
1435
  self._launcher_loss_task.cancel()
564
1436
  self._launcher_loss_task = None
565
1437
 
1438
+ # 取消多连接相关任务
1439
+ for name, task_dict in [("offer", self._offer_tasks),
1440
+ ("shrink", self._shrink_tasks),
1441
+ ("release", self._release_timers)]:
1442
+ if task_dict:
1443
+ print(f"[kernel] 取消 {len(task_dict)} 个 {name} 任务")
1444
+ for task in task_dict.values():
1445
+ if not task.done():
1446
+ task.cancel()
1447
+ task_dict.clear()
1448
+
566
1449
  # 关闭其他模块的 WebSocket 连接(保留 Launcher 连接)
567
- other_connections = {mid: ws for mid, ws in self.connections.items() if mid != "launcher"}
568
- if other_connections:
569
- print(f"[kernel] 关闭 {len(other_connections)} 个其他模块的 WebSocket 连接")
570
- for module_id, ws in other_connections.items():
571
- try:
572
- await ws.close(code=1001, reason="Server shutting down")
573
- except Exception as e:
574
- print(f"[kernel] 关闭连接失败 {module_id}: {e}")
1450
+ other_mids = [mid for mid in self.connections if mid != "launcher"]
1451
+ if other_mids:
1452
+ count = sum(len(self.connections.get(mid, {})) for mid in other_mids)
1453
+ print(f"[kernel] 关闭 {count} 个其他模块的 WebSocket 连接")
1454
+ for mid in other_mids:
1455
+ for slot, ws in list(self.connections.get(mid, {}).items()):
1456
+ try:
1457
+ await ws.close(code=1001, reason="Server shutting down")
1458
+ except Exception as e:
1459
+ print(f"[kernel] 关闭连接失败 {mid} slot {slot}: {e}")
575
1460
 
576
1461
  # 清空 RPC 转发队列
577
1462
  pending_count = len(self.rpc_router._pending)
@@ -606,13 +1491,15 @@ class KernelServer:
606
1491
  self._launcher_loss_task.cancel()
607
1492
  self._launcher_loss_task = None
608
1493
 
609
- # Close all WebSocket connections
610
- print(f"[kernel] Closing {len(self.connections)} WebSocket connections...")
611
- for module_id, ws in list(self.connections.items()):
612
- try:
613
- await ws.close(code=1001, reason="Server shutting down")
614
- except Exception as e:
615
- print(f"[kernel] Failed to close connection for {module_id}: {e}")
1494
+ # Close all WebSocket connections (multi-slot)
1495
+ total_slots = sum(len(s) for s in self.connections.values())
1496
+ print(f"[kernel] Closing {total_slots} WebSocket connections...")
1497
+ for module_id, slots in list(self.connections.items()):
1498
+ for slot, ws in list(slots.items()):
1499
+ try:
1500
+ await ws.close(code=1001, reason="Server shutting down")
1501
+ except Exception as e:
1502
+ print(f"[kernel] Failed to close connection for {module_id} slot {slot}: {e}")
616
1503
  self.connections.clear()
617
1504
 
618
1505
  # Clear pending RPC forwards