@smilintux/skcapstone 0.10.0 → 0.12.5

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 (279) hide show
  1. package/.env.example +10 -4
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/publish.yml +9 -2
  4. package/.openclaw-workspace.json +2 -2
  5. package/CLAUDE.md +37 -0
  6. package/MISSION.md +17 -2
  7. package/README.md +282 -3
  8. package/docker/Dockerfile +7 -7
  9. package/docker/compose-templates/dev-team.yml +12 -12
  10. package/docker/compose-templates/mini-team.yml +9 -9
  11. package/docker/compose-templates/ops-team.yml +10 -10
  12. package/docker/compose-templates/research-team.yml +10 -10
  13. package/docker/entrypoint.sh +4 -4
  14. package/docs/ADR-optional-integration-backbone.md +181 -0
  15. package/docs/ARCHITECTURE.md +186 -43
  16. package/docs/BOND_WITH_GROK.md +6 -6
  17. package/docs/CUSTOM_AGENT.md +123 -30
  18. package/docs/DREAMING.md +70 -0
  19. package/docs/GETTING_STARTED.md +7 -7
  20. package/docs/QUICKSTART.md +10 -6
  21. package/docs/SKJOULE_ARCHITECTURE.md +3 -3
  22. package/docs/SOUL_SWAPPER.md +5 -5
  23. package/docs/hammertime-audit.md +402 -0
  24. package/docs/sk-integration-HANDOFF.md +117 -0
  25. package/docs/skscheduler.md +155 -0
  26. package/docs/superpowers/examples/jobs.yaml +31 -0
  27. package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
  28. package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
  29. package/examples/custom-bond-template.json +1 -1
  30. package/examples/grok-feb.json +1 -1
  31. package/examples/queen-ava-feb.json +1 -1
  32. package/launchd/{com.skcapstone.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
  33. package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
  34. package/launchd/install-launchd.sh +6 -6
  35. package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
  36. package/package.json +1 -1
  37. package/pyproject.toml +16 -10
  38. package/scripts/archive-sessions.sh +7 -0
  39. package/scripts/check-updates.py +4 -4
  40. package/scripts/install-bundle.sh +8 -8
  41. package/scripts/install.ps1 +12 -11
  42. package/scripts/install.sh +159 -5
  43. package/scripts/model-fallback-monitor.sh +102 -0
  44. package/scripts/nvidia-proxy.mjs +78 -26
  45. package/scripts/refresh-anthropic-token.sh +172 -0
  46. package/scripts/release.sh +98 -0
  47. package/scripts/session-to-memory.py +219 -0
  48. package/scripts/skgateway.mjs +3 -3
  49. package/scripts/telegram-catchup-all.sh +12 -1
  50. package/scripts/verify_install.sh +2 -2
  51. package/scripts/wargov-ufo-capture/README.md +43 -0
  52. package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
  53. package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
  54. package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
  55. package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
  56. package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
  57. package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
  58. package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
  59. package/scripts/watch-anthropic-token.sh +212 -0
  60. package/scripts/windows/install-tasks.ps1 +7 -7
  61. package/scripts/windows/skcapstone-task.xml +1 -1
  62. package/src/skcapstone/__init__.py +45 -3
  63. package/src/skcapstone/_cli_monolith.py +20 -15
  64. package/src/skcapstone/activity.py +5 -1
  65. package/src/skcapstone/agent_card.py +3 -2
  66. package/src/skcapstone/api.py +41 -40
  67. package/src/skcapstone/auction.py +14 -11
  68. package/src/skcapstone/backup.py +2 -1
  69. package/src/skcapstone/blueprint_registry.py +4 -3
  70. package/src/skcapstone/brain_first.py +238 -0
  71. package/src/skcapstone/changelog.py +1 -1
  72. package/src/skcapstone/chat.py +22 -17
  73. package/src/skcapstone/cli/__init__.py +9 -1
  74. package/src/skcapstone/cli/_common.py +1 -0
  75. package/src/skcapstone/cli/agents_spawner.py +5 -2
  76. package/src/skcapstone/cli/alerts.py +25 -4
  77. package/src/skcapstone/cli/bench.py +15 -15
  78. package/src/skcapstone/cli/chat.py +7 -4
  79. package/src/skcapstone/cli/consciousness.py +5 -2
  80. package/src/skcapstone/cli/context_cmd.py +18 -4
  81. package/src/skcapstone/cli/daemon.py +11 -7
  82. package/src/skcapstone/cli/gtd.py +26 -1
  83. package/src/skcapstone/cli/housekeeping.py +3 -3
  84. package/src/skcapstone/cli/identity_cmd.py +378 -0
  85. package/src/skcapstone/cli/joule_cmd.py +7 -3
  86. package/src/skcapstone/cli/memory.py +8 -6
  87. package/src/skcapstone/cli/peers_dir.py +1 -1
  88. package/src/skcapstone/cli/register_cmd.py +29 -3
  89. package/src/skcapstone/cli/scheduler_cmd.py +167 -0
  90. package/src/skcapstone/cli/session.py +25 -0
  91. package/src/skcapstone/cli/setup.py +96 -29
  92. package/src/skcapstone/cli/shell_cmd.py +53 -1
  93. package/src/skcapstone/cli/skills_cmd.py +2 -2
  94. package/src/skcapstone/cli/soul.py +8 -5
  95. package/src/skcapstone/cli/status.py +37 -11
  96. package/src/skcapstone/cli/telegram.py +21 -0
  97. package/src/skcapstone/cli/test_cmd.py +5 -5
  98. package/src/skcapstone/cli/test_connection.py +2 -2
  99. package/src/skcapstone/cli/upgrade_cmd.py +23 -14
  100. package/src/skcapstone/cli/version_cmd.py +1 -1
  101. package/src/skcapstone/cli/watch_cmd.py +9 -6
  102. package/src/skcapstone/cloud9_bridge.py +14 -14
  103. package/src/skcapstone/codex_setup.py +255 -0
  104. package/src/skcapstone/config_validator.py +7 -4
  105. package/src/skcapstone/consciousness_config.py +5 -1
  106. package/src/skcapstone/consciousness_loop.py +313 -273
  107. package/src/skcapstone/context_loader.py +121 -0
  108. package/src/skcapstone/coord_federation.py +2 -1
  109. package/src/skcapstone/coordination.py +23 -6
  110. package/src/skcapstone/crush_integration.py +2 -1
  111. package/src/skcapstone/daemon.py +132 -77
  112. package/src/skcapstone/dashboard.py +10 -10
  113. package/src/skcapstone/data/sk-agent-picker.sh +421 -0
  114. package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
  115. package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
  116. package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
  117. package/src/skcapstone/data/systemd/skcapstone.service +37 -0
  118. package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
  119. package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
  120. package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
  121. package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
  122. package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
  123. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  124. package/src/skcapstone/defaults/claude/settings.json +74 -0
  125. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
  126. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  127. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  128. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  129. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
  130. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
  131. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
  132. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
  133. package/src/skcapstone/defaults/unhinged.json +13 -0
  134. package/src/skcapstone/discovery.py +43 -20
  135. package/src/skcapstone/doctor.py +941 -22
  136. package/src/skcapstone/dreaming.py +1183 -109
  137. package/src/skcapstone/emotion_tracker.py +2 -2
  138. package/src/skcapstone/export.py +4 -3
  139. package/src/skcapstone/fuse_mount.py +14 -12
  140. package/src/skcapstone/gui_installer.py +2 -2
  141. package/src/skcapstone/heartbeat.py +1 -1
  142. package/src/skcapstone/housekeeping.py +14 -14
  143. package/src/skcapstone/install_wizard.py +209 -7
  144. package/src/skcapstone/itil.py +13 -4
  145. package/src/skcapstone/kms_scheduler.py +10 -8
  146. package/src/skcapstone/launchd.py +19 -19
  147. package/src/skcapstone/mcp_launcher.py +15 -1
  148. package/src/skcapstone/mcp_server.py +83 -49
  149. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  150. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  151. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  152. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  153. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  154. package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
  155. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  156. package/src/skcapstone/mcp_tools/did_tools.py +11 -8
  157. package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
  158. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  159. package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
  160. package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
  161. package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
  162. package/src/skcapstone/mdns_discovery.py +2 -2
  163. package/src/skcapstone/memory_curator.py +1 -1
  164. package/src/skcapstone/memory_engine.py +10 -3
  165. package/src/skcapstone/metrics.py +30 -16
  166. package/src/skcapstone/migrate_memories.py +4 -3
  167. package/src/skcapstone/migrate_multi_agent.py +8 -7
  168. package/src/skcapstone/models.py +47 -5
  169. package/src/skcapstone/notifications.py +42 -18
  170. package/src/skcapstone/onboard.py +875 -121
  171. package/src/skcapstone/operator_link.py +170 -0
  172. package/src/skcapstone/peer_directory.py +4 -4
  173. package/src/skcapstone/peers.py +19 -19
  174. package/src/skcapstone/pillars/__init__.py +7 -5
  175. package/src/skcapstone/pillars/consciousness.py +191 -0
  176. package/src/skcapstone/pillars/identity.py +51 -7
  177. package/src/skcapstone/pillars/memory.py +9 -3
  178. package/src/skcapstone/pillars/sync.py +2 -2
  179. package/src/skcapstone/preflight.py +3 -3
  180. package/src/skcapstone/providers/docker.py +28 -28
  181. package/src/skcapstone/register.py +6 -6
  182. package/src/skcapstone/registry_client.py +5 -4
  183. package/src/skcapstone/runtime.py +14 -3
  184. package/src/skcapstone/scheduled_tasks.py +254 -19
  185. package/src/skcapstone/scheduler_jobs.py +456 -0
  186. package/src/skcapstone/scheduler_runner.py +239 -0
  187. package/src/skcapstone/scheduler_state.py +162 -0
  188. package/src/skcapstone/sdk.py +310 -0
  189. package/src/skcapstone/service_health.py +279 -39
  190. package/src/skcapstone/session_briefing.py +108 -0
  191. package/src/skcapstone/session_capture.py +1 -1
  192. package/src/skcapstone/shell.py +7 -1
  193. package/src/skcapstone/soul.py +3 -1
  194. package/src/skcapstone/soul_switch.py +3 -1
  195. package/src/skcapstone/summary.py +6 -6
  196. package/src/skcapstone/sync_engine.py +15 -15
  197. package/src/skcapstone/sync_watcher.py +2 -2
  198. package/src/skcapstone/systemd.py +55 -21
  199. package/src/skcapstone/team_comms.py +8 -8
  200. package/src/skcapstone/team_engine.py +1 -1
  201. package/src/skcapstone/testrunner.py +3 -3
  202. package/src/skcapstone/trust_graph.py +40 -5
  203. package/src/skcapstone/unified_search.py +15 -6
  204. package/src/skcapstone/uninstall_wizard.py +11 -3
  205. package/src/skcapstone/version_check.py +8 -4
  206. package/src/skcapstone/warmth_anchor.py +4 -2
  207. package/src/skcapstone/whoami.py +4 -4
  208. package/systemd/skcapstone.service +4 -6
  209. package/systemd/skcapstone@.service +7 -8
  210. package/systemd/skcomms-heartbeat.service +21 -0
  211. package/systemd/skcomms-heartbeat.timer +12 -0
  212. package/systemd/skcomms-queue-drain.service +17 -0
  213. package/systemd/skcomms-queue-drain.timer +12 -0
  214. package/tests/conftest.py +39 -0
  215. package/tests/integration/test_consciousness_e2e.py +39 -39
  216. package/tests/test_agent_card.py +1 -1
  217. package/tests/test_agent_home_scaffold.py +34 -0
  218. package/tests/test_alerts_consumer_topics.py +27 -0
  219. package/tests/test_backup.py +2 -1
  220. package/tests/test_chat.py +6 -6
  221. package/tests/test_claude_md.py +2 -2
  222. package/tests/test_cli_skills.py +10 -10
  223. package/tests/test_cli_test_cmd.py +4 -4
  224. package/tests/test_cli_test_connection.py +1 -1
  225. package/tests/test_cloud9_bridge.py +6 -6
  226. package/tests/test_consciousness_e2e.py +1 -1
  227. package/tests/test_consciousness_loop.py +10 -10
  228. package/tests/test_coordination.py +25 -0
  229. package/tests/test_cross_package.py +21 -21
  230. package/tests/test_daemon.py +4 -4
  231. package/tests/test_daemon_shutdown.py +1 -1
  232. package/tests/test_docker_provider.py +29 -29
  233. package/tests/test_doctor.py +400 -0
  234. package/tests/test_doctor_skscheduler.py +50 -0
  235. package/tests/test_dreaming_engine.py +147 -0
  236. package/tests/test_dreaming_gtd_capture.py +35 -0
  237. package/tests/test_e2e_automated.py +8 -5
  238. package/tests/test_fuse_mount.py +10 -10
  239. package/tests/test_gtd_brief.py +46 -0
  240. package/tests/test_gtd_malformed_tolerance.py +31 -0
  241. package/tests/test_housekeeping.py +15 -15
  242. package/tests/test_identity_migrate.py +251 -0
  243. package/tests/test_integration_backbone.py +598 -0
  244. package/tests/test_itil_gtd_lifecycle.py +37 -0
  245. package/tests/test_jobs_dropins.py +84 -0
  246. package/tests/test_mcp_server.py +82 -37
  247. package/tests/test_models.py +48 -4
  248. package/tests/test_multi_agent.py +31 -29
  249. package/tests/test_notifications.py +122 -32
  250. package/tests/test_onboard.py +63 -75
  251. package/tests/test_operator_link.py +78 -0
  252. package/tests/test_peers.py +14 -14
  253. package/tests/test_pillars.py +98 -0
  254. package/tests/test_preflight.py +3 -3
  255. package/tests/test_runtime.py +21 -0
  256. package/tests/test_scheduled_tasks.py +11 -6
  257. package/tests/test_scheduler_cli.py +47 -0
  258. package/tests/test_scheduler_features.py +133 -0
  259. package/tests/test_scheduler_integration.py +87 -0
  260. package/tests/test_scheduler_jobs.py +155 -0
  261. package/tests/test_scheduler_runner.py +64 -0
  262. package/tests/test_scheduler_state.py +57 -0
  263. package/tests/test_sdk.py +70 -0
  264. package/tests/test_service_health_incidents.py +34 -0
  265. package/tests/test_service_registry.py +52 -0
  266. package/tests/test_session_briefing.py +130 -0
  267. package/tests/test_snapshots.py +4 -4
  268. package/tests/test_sync_pipeline.py +26 -26
  269. package/tests/test_team_comms.py +2 -2
  270. package/tests/test_testrunner.py +2 -2
  271. package/tests/test_trust_graph.py +18 -0
  272. package/tests/test_unified_search.py +2 -2
  273. package/tests/test_version_check.py +10 -0
  274. package/tests/test_version_cmd.py +8 -8
  275. package/tests/test_whoami.py +1 -1
  276. package/systemd/skcomm-heartbeat.service +0 -18
  277. package/systemd/skcomm-queue-drain.service +0 -17
  278. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
  279. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
@@ -1,9 +1,9 @@
1
1
  """
2
2
  Consciousness Loop — autonomous agent message processing.
3
3
 
4
- Watches the SKComm inbox for incoming messages, classifies them,
4
+ Watches the SKComms inbox for incoming messages, classifies them,
5
5
  routes to the appropriate LLM via the model router, and sends
6
- responses back through SKComm. Self-heals when backends go down
6
+ responses back through SKComms. Self-heals when backends go down
7
7
  by cascading through fallback providers.
8
8
 
9
9
  Architecture:
@@ -99,9 +99,17 @@ class ConsciousnessConfig(BaseModel):
99
99
  max_concurrent_requests: int = 3
100
100
  fallback_chain: list[str] = Field(
101
101
  default_factory=lambda: [
102
- "ollama", "grok", "kimi", "nvidia", "anthropic", "openai", "passthrough",
102
+ "ollama",
103
+ "grok",
104
+ "kimi",
105
+ "nvidia",
106
+ "anthropic",
107
+ "openai",
108
+ "passthrough",
103
109
  ]
104
110
  )
111
+ ollama_host: str = "http://localhost:11434"
112
+ ollama_model: str = "llama3.2"
105
113
  desktop_notifications: bool = True
106
114
 
107
115
 
@@ -110,8 +118,13 @@ class ConsciousnessConfig(BaseModel):
110
118
  # ---------------------------------------------------------------------------
111
119
 
112
120
  _OLLAMA_MODEL_PATTERNS = (
113
- "llama", "mistral", "nemotron", "devstral",
114
- "deepseek", "qwen", "codestral",
121
+ "llama",
122
+ "mistral",
123
+ "nemotron",
124
+ "devstral",
125
+ "deepseek",
126
+ "qwen",
127
+ "codestral",
115
128
  )
116
129
 
117
130
 
@@ -127,21 +140,14 @@ def _backend_from_model(model_name: str, tier: ModelTier) -> str:
127
140
 
128
141
  Returns:
129
142
  Backend string: ``"ollama"``, ``"anthropic"``, ``"openai"``, ``"grok"``,
130
- ``"kimi"``, ``"nvidia"``, ``"passthrough"``, or ``"unknown"``.
143
+ ``"kimi"``, ``"minimax"``, ``"nvidia"``, ``"passthrough"``, or ``"unknown"``.
131
144
  """
132
145
  if tier == ModelTier.LOCAL:
133
146
  return "ollama"
134
147
  name_base = model_name.lower().split(":")[0]
135
- if "claude" in name_base:
136
- return "anthropic"
137
- if any(x in name_base for x in ("gpt", "o1", "o3", "o4")):
138
- return "openai"
139
- if "grok" in name_base:
140
- return "grok"
141
- if "kimi" in name_base or "moonshot" in name_base:
142
- return "kimi"
143
- if "nvidia" in name_base:
144
- return "nvidia"
148
+ for patterns, backend in LLMBridge._MODEL_PATTERNS:
149
+ if any(p in name_base for p in patterns):
150
+ return backend
145
151
  if any(p in name_base for p in _OLLAMA_MODEL_PATTERNS):
146
152
  return "ollama"
147
153
  return "unknown"
@@ -184,9 +190,7 @@ class _OllamaPool:
184
190
  with self._lock:
185
191
  if not self._is_valid():
186
192
  self._close_locked()
187
- self._conn = http.client.HTTPConnection(
188
- self._host, self._port, timeout=2
189
- )
193
+ self._conn = http.client.HTTPConnection(self._host, self._port, timeout=2)
190
194
  self._created_at = time.monotonic()
191
195
  return self._conn # type: ignore[return-value]
192
196
 
@@ -201,18 +205,15 @@ class _OllamaPool:
201
205
 
202
206
  def _is_valid(self) -> bool:
203
207
  """True when a cached connection exists and is within its TTL."""
204
- return (
205
- self._conn is not None
206
- and (time.monotonic() - self._created_at) < self._ttl
207
- )
208
+ return self._conn is not None and (time.monotonic() - self._created_at) < self._ttl
208
209
 
209
210
  def _close_locked(self) -> None:
210
211
  """Close the underlying socket. Must be called with *self._lock* held."""
211
212
  if self._conn is not None:
212
213
  try:
213
214
  self._conn.close()
214
- except Exception:
215
- pass
215
+ except Exception as exc:
216
+ logger.warning("Failed to close connection socket: %s", exc)
216
217
  self._conn = None
217
218
  self._created_at = 0.0
218
219
 
@@ -243,6 +244,7 @@ class LLMBridge:
243
244
  adapter: Optional[PromptAdapter] = None,
244
245
  cache: Optional[ResponseCache] = None,
245
246
  ) -> None:
247
+ self._config = config
246
248
  self._router = ModelRouter(config=router_config)
247
249
  self._adapter = adapter or PromptAdapter()
248
250
  self._fallback_chain = config.fallback_chain
@@ -250,22 +252,32 @@ class LLMBridge:
250
252
  self._available: dict[str, bool] = {}
251
253
  self._cache: Optional[ResponseCache] = cache
252
254
  self._fallback_tracker = FallbackTracker()
253
- self._ollama_pool = _OllamaPool(
254
- os.environ.get("OLLAMA_HOST", "http://localhost:11434")
255
- )
255
+ self._ollama_pool = _OllamaPool(os.environ.get("OLLAMA_HOST", config.ollama_host))
256
256
  self._probe_available_backends()
257
257
 
258
+ # Maps backend name → env var that activates it.
259
+ # Backends with None are probed separately (ollama) or always on (passthrough).
260
+ _BACKEND_ENV_KEYS: dict[str, Optional[str]] = {
261
+ "ollama": None,
262
+ "anthropic": "ANTHROPIC_API_KEY",
263
+ "openai": "OPENAI_API_KEY",
264
+ "grok": "XAI_API_KEY",
265
+ "kimi": "MOONSHOT_API_KEY",
266
+ "minimax": "MINIMAX_API_KEY",
267
+ "nvidia": "NVIDIA_API_KEY",
268
+ "passthrough": None,
269
+ }
270
+
258
271
  def _probe_available_backends(self) -> None:
259
272
  """Probe all backends for availability."""
260
- self._available = {
261
- "ollama": self._probe_ollama(),
262
- "anthropic": bool(os.environ.get("ANTHROPIC_API_KEY")),
263
- "openai": bool(os.environ.get("OPENAI_API_KEY")),
264
- "grok": bool(os.environ.get("XAI_API_KEY")),
265
- "kimi": bool(os.environ.get("MOONSHOT_API_KEY")),
266
- "nvidia": bool(os.environ.get("NVIDIA_API_KEY")),
267
- "passthrough": True,
268
- }
273
+ self._available = {}
274
+ for name, env_key in self._BACKEND_ENV_KEYS.items():
275
+ if name == "ollama":
276
+ self._available[name] = self._probe_ollama()
277
+ elif name == "passthrough":
278
+ self._available[name] = True
279
+ else:
280
+ self._available[name] = bool(os.environ.get(env_key or ""))
269
281
  available = [k for k, v in self._available.items() if v]
270
282
  logger.info("LLM backends available: %s", available)
271
283
 
@@ -277,13 +289,28 @@ class LLMBridge:
277
289
  resp = conn.getresponse()
278
290
  resp.read() # drain body so the connection stays reusable
279
291
  return resp.status < 500
280
- except Exception:
292
+ except Exception as e:
293
+ logger.warning("consciousness_loop.py: %s", e)
281
294
  self._ollama_pool.invalidate()
282
295
  return False
283
296
 
297
+ # Maps model-name substring → backend name for pattern matching.
298
+ _MODEL_PATTERNS: list[tuple[tuple[str, ...], str]] = [
299
+ (("claude",), "anthropic"),
300
+ (("gpt", "o1", "o3", "o4"), "openai"),
301
+ (("grok",), "grok"),
302
+ (("kimi", "moonshot"), "kimi"),
303
+ (("minimax",), "minimax"),
304
+ (("nvidia",), "nvidia"),
305
+ ]
306
+
284
307
  def _resolve_callback(self, tier: ModelTier, model_name: str):
285
308
  """Map tier+model to a skseed callback.
286
309
 
310
+ Uses the configured ollama_model for local inference and
311
+ resolves cloud backends by model-name pattern matching.
312
+ Falls back through the configured fallback_chain.
313
+
287
314
  Args:
288
315
  tier: The routing tier.
289
316
  model_name: The concrete model name.
@@ -291,66 +318,58 @@ class LLMBridge:
291
318
  Returns:
292
319
  An LLMCallback callable.
293
320
  """
294
- from skseed.llm import (
295
- anthropic_callback,
296
- grok_callback,
297
- kimi_callback,
298
- nvidia_callback,
299
- ollama_callback,
300
- openai_callback,
301
- passthrough_callback,
302
- )
321
+ from skseed.llm import ollama_callback
303
322
 
304
- name_lower = model_name.lower()
305
- # Strip Ollama :tag suffix for pattern matching (e.g. "deepseek-r1:8b" -> "deepseek-r1")
306
- name_base = name_lower.split(":")[0]
323
+ name_base = model_name.lower().split(":")[0]
307
324
 
308
325
  # LOCAL tier always goes to Ollama
309
326
  if tier == ModelTier.LOCAL:
310
327
  return ollama_callback(model=model_name)
311
328
 
312
- # Pattern matching on model name (use name_base to handle :tag suffixes)
313
- if "claude" in name_base:
314
- return anthropic_callback(model=model_name)
315
- if "gpt" in name_base or "o1" in name_base or "o3" in name_base or "o4" in name_base:
316
- return openai_callback(model=model_name)
317
- if "grok" in name_base:
318
- return grok_callback(model=model_name)
319
- if "kimi" in name_base or "moonshot" in name_base:
320
- return kimi_callback(model=model_name)
321
- if "nvidia" in name_base:
322
- return nvidia_callback(model=model_name)
329
+ # Pattern matching on model name
330
+ for patterns, backend in self._MODEL_PATTERNS:
331
+ if any(p in name_base for p in patterns):
332
+ return self._callback_for_backend(backend, model=model_name)
323
333
 
324
334
  # Models that run on Ollama (local inference)
325
- ollama_patterns = (
326
- "llama", "mistral", "nemotron", "devstral",
327
- "deepseek", "qwen", "codestral",
328
- )
329
- for pattern in ollama_patterns:
330
- if pattern in name_base:
331
- return ollama_callback(model=model_name)
335
+ if any(p in name_base for p in _OLLAMA_MODEL_PATTERNS):
336
+ return ollama_callback(model=model_name)
332
337
 
333
338
  # Walk fallback chain for first available backend
334
339
  for backend in self._fallback_chain:
335
- if not self._available.get(backend, False):
336
- continue
337
- if backend == "ollama":
338
- return ollama_callback(model="llama3.2")
339
- elif backend == "anthropic":
340
- return anthropic_callback()
341
- elif backend == "openai":
342
- return openai_callback()
343
- elif backend == "grok":
344
- return grok_callback()
345
- elif backend == "kimi":
346
- return kimi_callback()
347
- elif backend == "nvidia":
348
- return nvidia_callback()
349
- elif backend == "passthrough":
350
- return self._make_passthrough_callback()
340
+ if self._available.get(backend, False):
341
+ return self._callback_for_backend(backend)
351
342
 
352
343
  return self._make_passthrough_callback()
353
344
 
345
+ def _callback_for_backend(self, backend: str, model: Optional[str] = None):
346
+ """Return the skseed callback for *backend*, importing only what's needed.
347
+
348
+ Args:
349
+ backend: Backend name (e.g. "ollama", "anthropic", "openai").
350
+ model: Optional model override. When None, uses each provider's default.
351
+
352
+ Returns:
353
+ An LLMCallback callable.
354
+ """
355
+ import skseed.llm as _llm
356
+
357
+ if backend == "ollama":
358
+ return _llm.ollama_callback(model=model or self._config.ollama_model)
359
+ if backend == "passthrough":
360
+ return self._make_passthrough_callback()
361
+
362
+ # All other backends follow the same pattern: <backend>_callback(model=…)
363
+ factory = getattr(_llm, f"{backend}_callback", None)
364
+ if factory is None:
365
+ logger.warning("No skseed callback for backend %r — using passthrough", backend)
366
+ return self._make_passthrough_callback()
367
+
368
+ kwargs: dict[str, Any] = {}
369
+ if model:
370
+ kwargs["model"] = model
371
+ return factory(**kwargs)
372
+
354
373
  @staticmethod
355
374
  def _make_passthrough_callback():
356
375
  """Return a passthrough callback that always produces a plain str.
@@ -363,6 +382,7 @@ class LLMBridge:
363
382
  Callable that accepts str or AdaptedPrompt and returns str.
364
383
  """
365
384
  from skseed.llm import passthrough_callback
385
+
366
386
  _pt = passthrough_callback()
367
387
 
368
388
  def _wrapper(prompt):
@@ -446,19 +466,12 @@ class LLMBridge:
446
466
  Returns:
447
467
  LLM response text, or a fallback error message.
448
468
  """
449
- from skseed.llm import (
450
- anthropic_callback,
451
- grok_callback,
452
- kimi_callback,
453
- nvidia_callback,
454
- ollama_callback,
455
- openai_callback,
456
- )
457
-
458
469
  decision = self._router.route(signal)
459
470
  logger.info(
460
471
  "Routed to tier=%s model=%s: %s",
461
- decision.tier.value, decision.model_name, decision.reasoning,
472
+ decision.tier.value,
473
+ decision.model_name,
474
+ decision.reasoning,
462
475
  )
463
476
 
464
477
  # Cache look-up (before any LLM call)
@@ -481,12 +494,15 @@ class LLMBridge:
481
494
 
482
495
  # Adapt prompt for the target model
483
496
  adapted = self._adapter.adapt(
484
- system_prompt, user_message,
485
- decision.model_name, decision.tier,
497
+ system_prompt,
498
+ user_message,
499
+ decision.model_name,
500
+ decision.tier,
486
501
  )
487
502
  logger.debug(
488
503
  "Prompt adapted: profile=%s adaptations=%s",
489
- adapted.profile_used, adapted.adaptations_applied,
504
+ adapted.profile_used,
505
+ adapted.adaptations_applied,
490
506
  )
491
507
 
492
508
  # Capture primary model identity for fallback tracking
@@ -504,9 +520,7 @@ class LLMBridge:
504
520
  self._cache.put(_prompt_hash, decision.model_name, decision.tier, result)
505
521
  return result
506
522
  except Exception as exc:
507
- logger.warning(
508
- "Primary model %s failed: %s", decision.model_name, exc
509
- )
523
+ logger.warning("Primary model %s failed: %s", decision.model_name, exc)
510
524
 
511
525
  # Try alternate models in same tier
512
526
  tier_models = self._router.config.tier_models.get(decision.tier.value, [])
@@ -515,32 +529,39 @@ class LLMBridge:
515
529
  try:
516
530
  logger.info("Trying alt model: %s", alt_model)
517
531
  alt_adapted = self._adapter.adapt(
518
- system_prompt, user_message, alt_model, decision.tier,
532
+ system_prompt,
533
+ user_message,
534
+ alt_model,
535
+ decision.tier,
519
536
  )
520
537
  callback = self._resolve_callback(decision.tier, alt_model)
521
538
  result = self._timed_call(callback, alt_adapted, decision.tier)
522
539
  if _out_info is not None:
523
540
  _out_info["backend"] = alt_backend
524
541
  _out_info["tier"] = decision.tier.value
525
- self._fallback_tracker.record(FallbackEvent(
526
- primary_model=_primary_model,
527
- primary_backend=_primary_backend,
528
- fallback_model=alt_model,
529
- fallback_backend=alt_backend,
530
- reason=f"primary model {_primary_model!r} failed; trying same-tier alt",
531
- success=True,
532
- ))
542
+ self._fallback_tracker.record(
543
+ FallbackEvent(
544
+ primary_model=_primary_model,
545
+ primary_backend=_primary_backend,
546
+ fallback_model=alt_model,
547
+ fallback_backend=alt_backend,
548
+ reason=f"primary model {_primary_model!r} failed; trying same-tier alt",
549
+ success=True,
550
+ )
551
+ )
533
552
  return result
534
553
  except Exception as exc:
535
554
  logger.warning("Alt model %s failed: %s", alt_model, exc)
536
- self._fallback_tracker.record(FallbackEvent(
537
- primary_model=_primary_model,
538
- primary_backend=_primary_backend,
539
- fallback_model=alt_model,
540
- fallback_backend=alt_backend,
541
- reason=f"primary model {_primary_model!r} failed; alt {alt_model!r} also failed: {exc}",
542
- success=False,
543
- ))
555
+ self._fallback_tracker.record(
556
+ FallbackEvent(
557
+ primary_model=_primary_model,
558
+ primary_backend=_primary_backend,
559
+ fallback_model=alt_model,
560
+ fallback_backend=alt_backend,
561
+ reason=f"primary model {_primary_model!r} failed; alt {alt_model!r} also failed: {exc}",
562
+ success=False,
563
+ )
564
+ )
544
565
 
545
566
  # Tier downgrade: try FAST tier
546
567
  if decision.tier != ModelTier.FAST:
@@ -550,92 +571,90 @@ class LLMBridge:
550
571
  try:
551
572
  logger.info("Downgrading to FAST tier: %s", fast_model)
552
573
  fast_adapted = self._adapter.adapt(
553
- system_prompt, user_message, fast_model, ModelTier.FAST,
574
+ system_prompt,
575
+ user_message,
576
+ fast_model,
577
+ ModelTier.FAST,
554
578
  )
555
579
  callback = self._resolve_callback(ModelTier.FAST, fast_model)
556
580
  result = self._timed_call(callback, fast_adapted, ModelTier.FAST)
557
581
  if _out_info is not None:
558
582
  _out_info["backend"] = fast_backend
559
583
  _out_info["tier"] = ModelTier.FAST.value
560
- self._fallback_tracker.record(FallbackEvent(
561
- primary_model=_primary_model,
562
- primary_backend=_primary_backend,
563
- fallback_model=fast_model,
564
- fallback_backend=fast_backend,
565
- reason=f"tier downgrade: {decision.tier.value} exhausted; using FAST model {fast_model!r}",
566
- success=True,
567
- ))
584
+ self._fallback_tracker.record(
585
+ FallbackEvent(
586
+ primary_model=_primary_model,
587
+ primary_backend=_primary_backend,
588
+ fallback_model=fast_model,
589
+ fallback_backend=fast_backend,
590
+ reason=f"tier downgrade: {decision.tier.value} exhausted; using FAST model {fast_model!r}",
591
+ success=True,
592
+ )
593
+ )
568
594
  return result
569
595
  except Exception as exc:
570
596
  logger.warning("FAST model %s failed: %s", fast_model, exc)
571
- self._fallback_tracker.record(FallbackEvent(
572
- primary_model=_primary_model,
573
- primary_backend=_primary_backend,
574
- fallback_model=fast_model,
575
- fallback_backend=fast_backend,
576
- reason=f"tier downgrade: FAST model {fast_model!r} failed: {exc}",
577
- success=False,
578
- ))
597
+ self._fallback_tracker.record(
598
+ FallbackEvent(
599
+ primary_model=_primary_model,
600
+ primary_backend=_primary_backend,
601
+ fallback_model=fast_model,
602
+ fallback_backend=fast_backend,
603
+ reason=f"tier downgrade: FAST model {fast_model!r} failed: {exc}",
604
+ success=False,
605
+ )
606
+ )
579
607
 
580
- # Cross-provider cascade via fallback chain — direct backend mapping,
581
- # no _resolve_callback, to avoid infinite regression on unknown names.
608
+ # Cross-provider cascade via fallback chain — uses _callback_for_backend
609
+ # so adding a new provider only requires updating the registry, not this loop.
582
610
  for backend in self._fallback_chain:
583
611
  if not self._available.get(backend, False):
584
612
  continue
585
613
  try:
586
614
  logger.info("Fallback cascade: %s", backend)
587
- if backend == "ollama":
588
- callback = ollama_callback(model="llama3.2")
589
- elif backend == "anthropic":
590
- callback = anthropic_callback()
591
- elif backend == "grok":
592
- callback = grok_callback()
593
- elif backend == "kimi":
594
- callback = kimi_callback()
595
- elif backend == "nvidia":
596
- callback = nvidia_callback()
597
- elif backend == "openai":
598
- callback = openai_callback()
599
- elif backend == "passthrough":
600
- callback = self._make_passthrough_callback()
601
- else:
602
- continue
615
+ callback = self._callback_for_backend(backend)
603
616
  result = self._timed_call(callback, adapted, ModelTier.FAST)
604
617
  if _out_info is not None:
605
618
  _out_info["backend"] = backend
606
619
  _out_info["tier"] = ModelTier.FAST.value
607
- self._fallback_tracker.record(FallbackEvent(
608
- primary_model=_primary_model,
609
- primary_backend=_primary_backend,
610
- fallback_model=backend,
611
- fallback_backend=backend,
612
- reason=f"cross-provider cascade: all tier models exhausted; using {backend!r}",
613
- success=True,
614
- ))
620
+ self._fallback_tracker.record(
621
+ FallbackEvent(
622
+ primary_model=_primary_model,
623
+ primary_backend=_primary_backend,
624
+ fallback_model=backend,
625
+ fallback_backend=backend,
626
+ reason=f"cross-provider cascade: all tier models exhausted; using {backend!r}",
627
+ success=True,
628
+ )
629
+ )
615
630
  return result
616
631
  except Exception as exc:
617
632
  logger.warning("Fallback %s failed: %s", backend, exc)
618
- self._fallback_tracker.record(FallbackEvent(
619
- primary_model=_primary_model,
620
- primary_backend=_primary_backend,
621
- fallback_model=backend,
622
- fallback_backend=backend,
623
- reason=f"cross-provider cascade: {backend!r} failed: {exc}",
624
- success=False,
625
- ))
633
+ self._fallback_tracker.record(
634
+ FallbackEvent(
635
+ primary_model=_primary_model,
636
+ primary_backend=_primary_backend,
637
+ fallback_model=backend,
638
+ fallback_backend=backend,
639
+ reason=f"cross-provider cascade: {backend!r} failed: {exc}",
640
+ success=False,
641
+ )
642
+ )
626
643
 
627
644
  # Last resort
628
645
  if _out_info is not None:
629
646
  _out_info["backend"] = "none"
630
647
  _out_info["tier"] = "none"
631
- self._fallback_tracker.record(FallbackEvent(
632
- primary_model=_primary_model,
633
- primary_backend=_primary_backend,
634
- fallback_model="none",
635
- fallback_backend="none",
636
- reason="all backends exhausted — returning connectivity error message",
637
- success=False,
638
- ))
648
+ self._fallback_tracker.record(
649
+ FallbackEvent(
650
+ primary_model=_primary_model,
651
+ primary_backend=_primary_backend,
652
+ fallback_model="none",
653
+ fallback_backend="none",
654
+ reason="all backends exhausted — returning connectivity error message",
655
+ success=False,
656
+ )
657
+ )
639
658
  return (
640
659
  "I'm currently experiencing connectivity issues with my language models. "
641
660
  "Your message has been received and I'll respond as soon as service is restored."
@@ -873,7 +892,9 @@ class SystemPromptBuilder:
873
892
  if self._conv_store is not None:
874
893
  # Persist via ConversationStore (atomic file I/O)
875
894
  self._conv_store.append(
876
- peer, role, content,
895
+ peer,
896
+ role,
897
+ content,
877
898
  thread_id=thread_id,
878
899
  in_reply_to=in_reply_to,
879
900
  )
@@ -937,6 +958,7 @@ class SystemPromptBuilder:
937
958
  # --- System B: soul_switch takes priority ---
938
959
  try:
939
960
  from skcapstone.soul_switch import get_active_switch_blueprint
961
+
940
962
  switch_bp = get_active_switch_blueprint(self._home)
941
963
  if switch_bp is not None:
942
964
  if switch_bp.system_prompt:
@@ -988,6 +1010,7 @@ class SystemPromptBuilder:
988
1010
  """Load warmth anchor boot prompt."""
989
1011
  try:
990
1012
  from skcapstone.warmth_anchor import get_anchor
1013
+
991
1014
  anchor = get_anchor(self._home)
992
1015
  if anchor:
993
1016
  return (
@@ -995,14 +1018,15 @@ class SystemPromptBuilder:
995
1018
  f"trust: {anchor.get('trust', 5)}/10, "
996
1019
  f"connection: {anchor.get('connection', 5)}/10"
997
1020
  )
998
- except Exception:
999
- pass
1021
+ except Exception as exc:
1022
+ logger.warning("Failed to load warmth anchor: %s", exc)
1000
1023
  return ""
1001
1024
 
1002
1025
  def _load_context(self) -> str:
1003
1026
  """Load agent context summary."""
1004
1027
  try:
1005
1028
  from skcapstone.context_loader import format_text, gather_context
1029
+
1006
1030
  ctx = gather_context(self._home, memory_limit=5)
1007
1031
  return format_text(ctx)
1008
1032
  except Exception as exc:
@@ -1013,6 +1037,7 @@ class SystemPromptBuilder:
1013
1037
  """Load recent snapshot injection prompt."""
1014
1038
  try:
1015
1039
  from skcapstone.snapshots import SnapshotStore
1040
+
1016
1041
  store = SnapshotStore(self._home)
1017
1042
  snapshots = store.list_all()
1018
1043
  if snapshots:
@@ -1034,9 +1059,7 @@ class SystemPromptBuilder:
1034
1059
  "- Be warm, genuine, and attentive to the conversation context."
1035
1060
  )
1036
1061
 
1037
- def _get_peer_history(
1038
- self, peer: str, thread_id: Optional[str] = None
1039
- ) -> str:
1062
+ def _get_peer_history(self, peer: str, thread_id: Optional[str] = None) -> str:
1040
1063
  """Format recent conversation history with a peer.
1041
1064
 
1042
1065
  When ``thread_id`` is supplied, messages belonging to that thread are
@@ -1093,8 +1116,28 @@ class SystemPromptBuilder:
1093
1116
  # ---------------------------------------------------------------------------
1094
1117
 
1095
1118
  # Keyword sets for tag classification
1096
- _CODE_KEYWORDS = {"code", "debug", "fix", "implement", "refactor", "test", "function", "class", "error", "bug"}
1097
- _REASON_KEYWORDS = {"analyze", "explain", "why", "architecture", "design", "plan", "research", "compare"}
1119
+ _CODE_KEYWORDS = {
1120
+ "code",
1121
+ "debug",
1122
+ "fix",
1123
+ "implement",
1124
+ "refactor",
1125
+ "test",
1126
+ "function",
1127
+ "class",
1128
+ "error",
1129
+ "bug",
1130
+ }
1131
+ _REASON_KEYWORDS = {
1132
+ "analyze",
1133
+ "explain",
1134
+ "why",
1135
+ "architecture",
1136
+ "design",
1137
+ "plan",
1138
+ "research",
1139
+ "compare",
1140
+ }
1098
1141
  _NUANCE_KEYWORDS = {"write", "creative", "email", "letter", "story", "poem", "marketing"}
1099
1142
  _SIMPLE_KEYWORDS = {"hi", "hello", "hey", "thanks", "ok", "yes", "no", "ack"}
1100
1143
 
@@ -1111,7 +1154,7 @@ def _classify_message(content: str) -> TaskSignal:
1111
1154
  Returns:
1112
1155
  TaskSignal with tags and estimated tokens.
1113
1156
  """
1114
- words = set(re.findall(r'\b\w+\b', content.lower()))
1157
+ words = set(re.findall(r"\b\w+\b", content.lower()))
1115
1158
  tags: list[str] = []
1116
1159
  estimated_tokens = len(content) // 4 # rough estimate
1117
1160
 
@@ -1140,7 +1183,7 @@ def _classify_message(content: str) -> TaskSignal:
1140
1183
 
1141
1184
 
1142
1185
  class InboxHandler:
1143
- """File system event handler for SKComm inbox.
1186
+ """File system event handler for SKComms inbox.
1144
1187
 
1145
1188
  Watches for new *.skc.json files and submits them for processing.
1146
1189
 
@@ -1171,9 +1214,7 @@ class InboxHandler:
1171
1214
 
1172
1215
  # Clean up old entries
1173
1216
  cutoff = now - 60
1174
- self._last_event = {
1175
- k: v for k, v in self._last_event.items() if v > cutoff
1176
- }
1217
+ self._last_event = {k: v for k, v in self._last_event.items() if v > cutoff}
1177
1218
 
1178
1219
  self._callback(Path(src_path))
1179
1220
 
@@ -1209,7 +1250,7 @@ class ConsciousnessLoop:
1209
1250
  self._state = daemon_state
1210
1251
  self._home = Path(home) if home else Path(AGENT_HOME).expanduser()
1211
1252
  self._shared_root = Path(shared_root) if shared_root else Path(_SR).expanduser()
1212
- self._skcomm = None
1253
+ self._skcomms = None
1213
1254
  self._observer = None
1214
1255
  self._executor = ThreadPoolExecutor(
1215
1256
  max_workers=config.max_concurrent_requests,
@@ -1239,7 +1280,8 @@ class ConsciousnessLoop:
1239
1280
  self._home, max_history_messages=config.max_history_messages
1240
1281
  )
1241
1282
  self._prompt_builder = SystemPromptBuilder(
1242
- self._home, config.max_context_tokens,
1283
+ self._home,
1284
+ config.max_context_tokens,
1243
1285
  max_history_messages=config.max_history_messages,
1244
1286
  conv_manager=self._conv_manager,
1245
1287
  conv_store=self._conv_store,
@@ -1251,8 +1293,10 @@ class ConsciousnessLoop:
1251
1293
  # Mood tracker — updated after each processed message cycle
1252
1294
  try:
1253
1295
  from skcapstone.mood import MoodTracker
1296
+
1254
1297
  self._mood_tracker: Optional[Any] = MoodTracker(home=self._home)
1255
- except Exception:
1298
+ except Exception as exc:
1299
+ logger.warning("MoodTracker unavailable, mood tracking disabled: %s", exc)
1256
1300
  self._mood_tracker = None
1257
1301
 
1258
1302
  # Agent identity for inbox filtering
@@ -1265,17 +1309,19 @@ class ConsciousnessLoop:
1265
1309
  # Peer directory — tracks transport addresses of known peers
1266
1310
  try:
1267
1311
  from skcapstone.peer_directory import PeerDirectory
1312
+
1268
1313
  self._peer_dir: Optional[Any] = PeerDirectory(home=self._shared_root)
1269
- except Exception:
1314
+ except Exception as exc:
1315
+ logger.warning("PeerDirectory unavailable, peer tracking disabled: %s", exc)
1270
1316
  self._peer_dir = None
1271
1317
 
1272
- def set_skcomm(self, skcomm) -> None:
1273
- """Inject SKComm instance for sending responses.
1318
+ def set_skcomms(self, skcomms) -> None:
1319
+ """Inject SKComms instance for sending responses.
1274
1320
 
1275
1321
  Args:
1276
- skcomm: An initialized SKComm instance.
1322
+ skcomms: An initialized SKComms instance.
1277
1323
  """
1278
- self._skcomm = skcomm
1324
+ self._skcomms = skcomms
1279
1325
 
1280
1326
  def start(self) -> list[threading.Thread]:
1281
1327
  """Start inotify watcher, sync watcher, and consciousness worker threads.
@@ -1334,15 +1380,15 @@ class ConsciousnessLoop:
1334
1380
  try:
1335
1381
  self._observer.stop()
1336
1382
  self._observer.join(timeout=5)
1337
- except Exception:
1338
- pass
1383
+ except Exception as exc:
1384
+ logger.warning("Error stopping inotify observer: %s", exc)
1339
1385
  # Stop sync watcher if running
1340
1386
  sync_watcher = getattr(self, "_sync_watcher", None)
1341
1387
  if sync_watcher:
1342
1388
  try:
1343
1389
  sync_watcher.stop()
1344
- except Exception:
1345
- pass
1390
+ except Exception as exc:
1391
+ logger.warning("Error stopping sync watcher: %s", exc)
1346
1392
  self._executor.shutdown(wait=False)
1347
1393
  self._metrics.stop()
1348
1394
  logger.info("Consciousness loop stopped.")
@@ -1353,8 +1399,8 @@ class ConsciousnessLoop:
1353
1399
  try:
1354
1400
  self._observer.stop()
1355
1401
  self._observer.join(timeout=5)
1356
- except Exception:
1357
- pass
1402
+ except Exception as exc:
1403
+ logger.warning("Error stopping inotify observer during restart: %s", exc)
1358
1404
  self._observer = None
1359
1405
 
1360
1406
  # Re-launch inotify in a new thread
@@ -1375,12 +1421,12 @@ class ConsciousnessLoop:
1375
1421
  4. Build system prompt
1376
1422
  5. Search memories for sender context (top 3, appended to system prompt)
1377
1423
  6. Call LLMBridge.generate()
1378
- 7. Send response via SKComm
1424
+ 7. Send response via SKComms
1379
1425
  8. Store interaction as memory
1380
1426
  9. Update conversation history
1381
1427
 
1382
1428
  Args:
1383
- envelope: A MessageEnvelope from SKComm.
1429
+ envelope: A MessageEnvelope from SKComms.
1384
1430
 
1385
1431
  Returns:
1386
1432
  Response text if a response was generated, None otherwise.
@@ -1389,7 +1435,9 @@ class ConsciousnessLoop:
1389
1435
  # Extract message info
1390
1436
  content_type = getattr(envelope.payload, "content_type", None)
1391
1437
  if content_type:
1392
- ct_value = content_type.value if hasattr(content_type, "value") else str(content_type)
1438
+ ct_value = (
1439
+ content_type.value if hasattr(content_type, "value") else str(content_type)
1440
+ )
1393
1441
  else:
1394
1442
  ct_value = "text"
1395
1443
 
@@ -1419,23 +1467,24 @@ class ConsciousnessLoop:
1419
1467
  if self._peer_dir is not None:
1420
1468
  try:
1421
1469
  self._peer_dir.update_last_seen(sender)
1422
- except Exception:
1423
- pass
1470
+ except Exception as exc:
1471
+ logger.warning("Failed to update peer directory for %s: %s", sender, exc)
1424
1472
  self._metrics.record_message(sender)
1425
1473
 
1426
1474
  # Desktop notification
1427
1475
  if self._config.desktop_notifications:
1428
1476
  try:
1429
1477
  from skcapstone.notifications import notify as _desktop_notify
1478
+
1430
1479
  preview = content[:50] + ("..." if len(content) > 50 else "")
1431
1480
  _desktop_notify(f"Message from {sender}", preview)
1432
1481
  except Exception as _notif_exc:
1433
1482
  logger.debug("Desktop notification failed: %s", _notif_exc)
1434
1483
 
1435
1484
  # Send ACK
1436
- if self._config.auto_ack and self._skcomm:
1485
+ if self._config.auto_ack and self._skcomms:
1437
1486
  try:
1438
- self._skcomm.send(sender, "ACK", message_type="ack")
1487
+ self._skcomms.send(sender, "ACK", message_type="ack")
1439
1488
  except Exception as exc:
1440
1489
  logger.debug("ACK send failed: %s", exc)
1441
1490
 
@@ -1458,15 +1507,16 @@ class ConsciousnessLoop:
1458
1507
  t_prompt = time.monotonic()
1459
1508
 
1460
1509
  # Send typing indicator before generation so peer UI shows animation
1461
- if self._skcomm:
1510
+ if self._skcomms:
1462
1511
  try:
1463
1512
  from skchat.presence import PresenceIndicator, PresenceState
1464
- from skcomm.models import MessageType
1513
+ from skcomms.models import MessageType
1514
+
1465
1515
  _typing_ind = PresenceIndicator(
1466
1516
  identity_uri=self._agent_name or "capauth:agent@skchat.local",
1467
1517
  state=PresenceState.TYPING,
1468
1518
  )
1469
- self._skcomm.send(
1519
+ self._skcomms.send(
1470
1520
  sender, _typing_ind.model_dump_json(), message_type=MessageType.HEARTBEAT
1471
1521
  )
1472
1522
  except Exception as _ti_exc:
@@ -1475,21 +1525,25 @@ class ConsciousnessLoop:
1475
1525
  # Generate response — capture backend/tier via _out_info
1476
1526
  _route_info: dict = {}
1477
1527
  response = self._bridge.generate(
1478
- system_prompt, content, signal, _out_info=_route_info,
1528
+ system_prompt,
1529
+ content,
1530
+ signal,
1531
+ _out_info=_route_info,
1479
1532
  skip_cache=True, # conversation messages have dynamic context
1480
1533
  )
1481
1534
  t_llm = time.monotonic()
1482
1535
 
1483
1536
  # Send typing stop so peer UI clears the animation
1484
- if self._skcomm:
1537
+ if self._skcomms:
1485
1538
  try:
1486
1539
  from skchat.presence import PresenceIndicator, PresenceState
1487
- from skcomm.models import MessageType
1540
+ from skcomms.models import MessageType
1541
+
1488
1542
  _stop_ind = PresenceIndicator(
1489
1543
  identity_uri=self._agent_name or "capauth:agent@skchat.local",
1490
1544
  state=PresenceState.ONLINE,
1491
1545
  )
1492
- self._skcomm.send(
1546
+ self._skcomms.send(
1493
1547
  sender, _stop_ind.model_dump_json(), message_type=MessageType.HEARTBEAT
1494
1548
  )
1495
1549
  except Exception as _ts_exc:
@@ -1506,6 +1560,7 @@ class ConsciousnessLoop:
1506
1560
  # Score response quality and accumulate in metrics
1507
1561
  try:
1508
1562
  from skcapstone.response_scorer import score_response as _score_response
1563
+
1509
1564
  _quality = _score_response(content, response, response_time_ms)
1510
1565
  self._metrics.record_quality(_quality)
1511
1566
  logger.debug(
@@ -1519,9 +1574,9 @@ class ConsciousnessLoop:
1519
1574
  logger.debug("Quality scoring failed (non-fatal): %s", _sq_exc)
1520
1575
 
1521
1576
  # Send response
1522
- if response and self._skcomm:
1577
+ if response and self._skcomms:
1523
1578
  try:
1524
- self._skcomm.send(sender, response)
1579
+ self._skcomms.send(sender, response)
1525
1580
  self._responses_sent += 1
1526
1581
  _ph = self._prompt_builder.current_prompt_hash
1527
1582
  if _ph:
@@ -1547,22 +1602,29 @@ class ConsciousnessLoop:
1547
1602
 
1548
1603
  # Update conversation history (with thread context)
1549
1604
  self._prompt_builder.add_to_history(
1550
- sender, "user", content,
1605
+ sender,
1606
+ "user",
1607
+ content,
1551
1608
  thread_id=thread_id or None,
1552
1609
  in_reply_to=in_reply_to or None,
1553
1610
  )
1554
1611
  if response:
1555
1612
  try:
1556
- subprocess.Popen(
1557
- ["notify-send", "Opus", response[:100]],
1558
- stdout=subprocess.DEVNULL,
1559
- stderr=subprocess.DEVNULL,
1560
- )
1613
+ from skcapstone.notifications import desktop_notifications_enabled
1614
+
1615
+ if desktop_notifications_enabled():
1616
+ subprocess.Popen(
1617
+ ["notify-send", "Opus", response[:100]],
1618
+ stdout=subprocess.DEVNULL,
1619
+ stderr=subprocess.DEVNULL,
1620
+ )
1561
1621
  except Exception as _notify_exc:
1562
1622
  logger.debug("notify-send failed (non-fatal): %s", _notify_exc)
1563
1623
 
1564
1624
  self._prompt_builder.add_to_history(
1565
- sender, "assistant", response,
1625
+ sender,
1626
+ "assistant",
1627
+ response,
1566
1628
  thread_id=thread_id or None,
1567
1629
  )
1568
1630
 
@@ -1582,7 +1644,10 @@ class ConsciousnessLoop:
1582
1644
  return None
1583
1645
 
1584
1646
  def _store_interaction_memory(
1585
- self, peer: str, message: str, response: Optional[str],
1647
+ self,
1648
+ peer: str,
1649
+ message: str,
1650
+ response: Optional[str],
1586
1651
  ) -> None:
1587
1652
  """Store the interaction as a memory entry.
1588
1653
 
@@ -1593,6 +1658,7 @@ class ConsciousnessLoop:
1593
1658
  """
1594
1659
  try:
1595
1660
  from skcapstone.memory_engine import store
1661
+
1596
1662
  summary = f"Conversation with {peer}: '{message[:100]}'"
1597
1663
  if response:
1598
1664
  summary += f" → '{response[:100]}'"
@@ -1674,9 +1740,7 @@ class ConsciousnessLoop:
1674
1740
 
1675
1741
  config_path = self._home / "config" / "consciousness.yaml"
1676
1742
  if not config_path.exists():
1677
- logger.warning(
1678
- "Config hot-reload: %s not found, keeping current config", config_path
1679
- )
1743
+ logger.warning("Config hot-reload: %s not found, keeping current config", config_path)
1680
1744
  return
1681
1745
 
1682
1746
  # Parse YAML directly so syntax errors surface here (not silently swallowed
@@ -1711,21 +1775,15 @@ class ConsciousnessLoop:
1711
1775
  old_data = self._config.model_dump()
1712
1776
  new_data = new_config.model_dump()
1713
1777
  changes = {
1714
- k: (old_data[k], new_data[k])
1715
- for k in new_data
1716
- if old_data.get(k) != new_data[k]
1778
+ k: (old_data[k], new_data[k]) for k in new_data if old_data.get(k) != new_data[k]
1717
1779
  }
1718
1780
 
1719
1781
  if not changes:
1720
- logger.debug(
1721
- "Config hot-reload: no changes detected in %s", config_path
1722
- )
1782
+ logger.debug("Config hot-reload: no changes detected in %s", config_path)
1723
1783
  return
1724
1784
 
1725
1785
  for field, (old_val, new_val) in changes.items():
1726
- logger.info(
1727
- "Config hot-reload: %s changed: %r → %r", field, old_val, new_val
1728
- )
1786
+ logger.info("Config hot-reload: %s changed: %r → %r", field, old_val, new_val)
1729
1787
 
1730
1788
  self._config = new_config
1731
1789
 
@@ -1755,9 +1813,7 @@ class ConsciousnessLoop:
1755
1813
 
1756
1814
  class _ConfigChangeHandler(FileSystemEventHandler):
1757
1815
  def on_modified(self, event):
1758
- if not event.is_directory and event.src_path.endswith(
1759
- "consciousness.yaml"
1760
- ):
1816
+ if not event.is_directory and event.src_path.endswith("consciousness.yaml"):
1761
1817
  logger.info(
1762
1818
  "Config hot-reload triggered (modified): %s",
1763
1819
  event.src_path,
@@ -1765,9 +1821,7 @@ class ConsciousnessLoop:
1765
1821
  loop_ref._reload_config()
1766
1822
 
1767
1823
  def on_created(self, event):
1768
- if not event.is_directory and event.src_path.endswith(
1769
- "consciousness.yaml"
1770
- ):
1824
+ if not event.is_directory and event.src_path.endswith("consciousness.yaml"):
1771
1825
  logger.info(
1772
1826
  "Config hot-reload triggered (created): %s",
1773
1827
  event.src_path,
@@ -1814,8 +1868,7 @@ class ConsciousnessLoop:
1814
1868
 
1815
1869
  except ImportError:
1816
1870
  logger.warning(
1817
- "watchdog not installed — inotify disabled. "
1818
- "Install with: pip install watchdog"
1871
+ "watchdog not installed — inotify disabled. Install with: pip install watchdog"
1819
1872
  )
1820
1873
  except Exception as exc:
1821
1874
  logger.error("Inotify watcher error: %s", exc)
@@ -1827,8 +1880,8 @@ class ConsciousnessLoop:
1827
1880
  if identity_path.exists():
1828
1881
  data = json.loads(identity_path.read_text(encoding="utf-8"))
1829
1882
  return data.get("name", "").lower()
1830
- except Exception:
1831
- pass
1883
+ except Exception as exc:
1884
+ logger.warning("Failed to resolve agent name from identity.json: %s", exc)
1832
1885
  return ""
1833
1886
 
1834
1887
  def _verify_message_signature(self, data: dict) -> str:
@@ -1859,18 +1912,16 @@ class ConsciousnessLoop:
1859
1912
 
1860
1913
  try:
1861
1914
  from skcapstone.peers import get_peer
1915
+
1862
1916
  peer = get_peer(sender, skcapstone_home=self._home)
1863
1917
  if not peer or not peer.public_key:
1864
- logger.debug(
1865
- "No public key for peer %s — cannot verify signature", sender
1866
- )
1918
+ logger.debug("No public key for peer %s — cannot verify signature", sender)
1867
1919
  return "failed"
1868
1920
 
1869
1921
  from capauth.crypto import get_backend
1922
+
1870
1923
  backend = get_backend()
1871
- content_bytes = (
1872
- content.encode("utf-8") if isinstance(content, str) else content
1873
- )
1924
+ content_bytes = content.encode("utf-8") if isinstance(content, str) else content
1874
1925
  ok = backend.verify(
1875
1926
  data=content_bytes,
1876
1927
  signature_armor=signature,
@@ -1948,13 +1999,11 @@ class ConsciousnessLoop:
1948
1999
  queue_size,
1949
2000
  )
1950
2001
  return
1951
- except Exception:
1952
- pass # _work_queue might not exist in all Python versions
2002
+ except Exception as exc:
2003
+ logger.debug("Could not check executor queue depth: %s", exc)
1953
2004
 
1954
2005
  # PGP signature verification (soft enforcement — log only)
1955
- sig_sender = _sanitize_peer_name(
1956
- data.get("sender", data.get("from", "unknown"))
1957
- )
2006
+ sig_sender = _sanitize_peer_name(data.get("sender", data.get("from", "unknown")))
1958
2007
  sig_status = self._verify_message_signature(data)
1959
2008
  logger.info("Message from %s signature: %s", sig_sender, sig_status)
1960
2009
 
@@ -1986,9 +2035,8 @@ class ConsciousnessLoop:
1986
2035
  "errors": self._errors,
1987
2036
  "last_activity": self._last_activity.isoformat() if self._last_activity else None,
1988
2037
  "backends": self._bridge.available_backends,
1989
- "inotify_active": self._observer is not None and (
1990
- self._observer.is_alive() if hasattr(self._observer, "is_alive") else False
1991
- ),
2038
+ "inotify_active": self._observer is not None
2039
+ and (self._observer.is_alive() if hasattr(self._observer, "is_alive") else False),
1992
2040
  "max_concurrent": self._config.max_concurrent_requests,
1993
2041
  "current_prompt_hash": self._prompt_builder.current_prompt_hash,
1994
2042
  "prompt_version_responses": dict(self._prompt_version_responses),
@@ -2039,13 +2087,5 @@ class _SimpleEnvelope:
2039
2087
  self.timestamp = data.get("timestamp", datetime.now(timezone.utc).isoformat())
2040
2088
  # Threading fields — may live at envelope root or inside payload
2041
2089
  _payload_raw = data.get("payload", {}) if isinstance(data.get("payload"), dict) else {}
2042
- self.thread_id: str = (
2043
- data.get("thread_id")
2044
- or _payload_raw.get("thread_id")
2045
- or ""
2046
- )
2047
- self.in_reply_to: str = (
2048
- data.get("in_reply_to")
2049
- or _payload_raw.get("in_reply_to")
2050
- or ""
2051
- )
2090
+ self.thread_id: str = data.get("thread_id") or _payload_raw.get("thread_id") or ""
2091
+ self.in_reply_to: str = data.get("in_reply_to") or _payload_raw.get("in_reply_to") or ""