@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.
- package/.env.example +10 -4
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +9 -2
- package/.openclaw-workspace.json +2 -2
- package/CLAUDE.md +37 -0
- package/MISSION.md +17 -2
- package/README.md +282 -3
- package/docker/Dockerfile +7 -7
- package/docker/compose-templates/dev-team.yml +12 -12
- package/docker/compose-templates/mini-team.yml +9 -9
- package/docker/compose-templates/ops-team.yml +10 -10
- package/docker/compose-templates/research-team.yml +10 -10
- package/docker/entrypoint.sh +4 -4
- package/docs/ADR-optional-integration-backbone.md +181 -0
- package/docs/ARCHITECTURE.md +186 -43
- package/docs/BOND_WITH_GROK.md +6 -6
- package/docs/CUSTOM_AGENT.md +123 -30
- package/docs/DREAMING.md +70 -0
- package/docs/GETTING_STARTED.md +7 -7
- package/docs/QUICKSTART.md +10 -6
- package/docs/SKJOULE_ARCHITECTURE.md +3 -3
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/docs/sk-integration-HANDOFF.md +117 -0
- package/docs/skscheduler.md +155 -0
- package/docs/superpowers/examples/jobs.yaml +31 -0
- package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
- package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
- package/examples/custom-bond-template.json +1 -1
- package/examples/grok-feb.json +1 -1
- package/examples/queen-ava-feb.json +1 -1
- package/launchd/{com.skcapstone.skcomm-heartbeat.plist → com.skcapstone.skcomms-heartbeat.plist} +4 -4
- package/launchd/{com.skcapstone.skcomm-queue-drain.plist → com.skcapstone.skcomms-queue-drain.plist} +4 -4
- package/launchd/install-launchd.sh +6 -6
- package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
- package/package.json +1 -1
- package/pyproject.toml +16 -10
- package/scripts/archive-sessions.sh +7 -0
- package/scripts/check-updates.py +4 -4
- package/scripts/install-bundle.sh +8 -8
- package/scripts/install.ps1 +12 -11
- package/scripts/install.sh +159 -5
- package/scripts/model-fallback-monitor.sh +102 -0
- package/scripts/nvidia-proxy.mjs +78 -26
- package/scripts/refresh-anthropic-token.sh +172 -0
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +219 -0
- package/scripts/skgateway.mjs +3 -3
- package/scripts/telegram-catchup-all.sh +12 -1
- package/scripts/verify_install.sh +2 -2
- package/scripts/wargov-ufo-capture/README.md +43 -0
- package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
- package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
- package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
- package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
- package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
- package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
- package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
- package/scripts/watch-anthropic-token.sh +212 -0
- package/scripts/windows/install-tasks.ps1 +7 -7
- package/scripts/windows/skcapstone-task.xml +1 -1
- package/src/skcapstone/__init__.py +45 -3
- package/src/skcapstone/_cli_monolith.py +20 -15
- package/src/skcapstone/activity.py +5 -1
- package/src/skcapstone/agent_card.py +3 -2
- package/src/skcapstone/api.py +41 -40
- package/src/skcapstone/auction.py +14 -11
- package/src/skcapstone/backup.py +2 -1
- package/src/skcapstone/blueprint_registry.py +4 -3
- package/src/skcapstone/brain_first.py +238 -0
- package/src/skcapstone/changelog.py +1 -1
- package/src/skcapstone/chat.py +22 -17
- package/src/skcapstone/cli/__init__.py +9 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/agents_spawner.py +5 -2
- package/src/skcapstone/cli/alerts.py +25 -4
- package/src/skcapstone/cli/bench.py +15 -15
- package/src/skcapstone/cli/chat.py +7 -4
- package/src/skcapstone/cli/consciousness.py +5 -2
- package/src/skcapstone/cli/context_cmd.py +18 -4
- package/src/skcapstone/cli/daemon.py +11 -7
- package/src/skcapstone/cli/gtd.py +26 -1
- package/src/skcapstone/cli/housekeeping.py +3 -3
- package/src/skcapstone/cli/identity_cmd.py +378 -0
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +8 -6
- package/src/skcapstone/cli/peers_dir.py +1 -1
- package/src/skcapstone/cli/register_cmd.py +29 -3
- package/src/skcapstone/cli/scheduler_cmd.py +167 -0
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -29
- package/src/skcapstone/cli/shell_cmd.py +53 -1
- package/src/skcapstone/cli/skills_cmd.py +2 -2
- package/src/skcapstone/cli/soul.py +8 -5
- package/src/skcapstone/cli/status.py +37 -11
- package/src/skcapstone/cli/telegram.py +21 -0
- package/src/skcapstone/cli/test_cmd.py +5 -5
- package/src/skcapstone/cli/test_connection.py +2 -2
- package/src/skcapstone/cli/upgrade_cmd.py +23 -14
- package/src/skcapstone/cli/version_cmd.py +1 -1
- package/src/skcapstone/cli/watch_cmd.py +9 -6
- package/src/skcapstone/cloud9_bridge.py +14 -14
- package/src/skcapstone/codex_setup.py +255 -0
- package/src/skcapstone/config_validator.py +7 -4
- package/src/skcapstone/consciousness_config.py +5 -1
- package/src/skcapstone/consciousness_loop.py +313 -273
- package/src/skcapstone/context_loader.py +121 -0
- package/src/skcapstone/coord_federation.py +2 -1
- package/src/skcapstone/coordination.py +23 -6
- package/src/skcapstone/crush_integration.py +2 -1
- package/src/skcapstone/daemon.py +132 -77
- package/src/skcapstone/dashboard.py +10 -10
- package/src/skcapstone/data/sk-agent-picker.sh +421 -0
- package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
- package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
- package/src/skcapstone/data/systemd/skcapstone.service +37 -0
- package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
- package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
- package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
- package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
- package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +43 -20
- package/src/skcapstone/doctor.py +941 -22
- package/src/skcapstone/dreaming.py +1183 -109
- package/src/skcapstone/emotion_tracker.py +2 -2
- package/src/skcapstone/export.py +4 -3
- package/src/skcapstone/fuse_mount.py +14 -12
- package/src/skcapstone/gui_installer.py +2 -2
- package/src/skcapstone/heartbeat.py +1 -1
- package/src/skcapstone/housekeeping.py +14 -14
- package/src/skcapstone/install_wizard.py +209 -7
- package/src/skcapstone/itil.py +13 -4
- package/src/skcapstone/kms_scheduler.py +10 -8
- package/src/skcapstone/launchd.py +19 -19
- package/src/skcapstone/mcp_launcher.py +15 -1
- package/src/skcapstone/mcp_server.py +83 -49
- package/src/skcapstone/mcp_tools/__init__.py +2 -0
- package/src/skcapstone/mcp_tools/_helpers.py +2 -2
- package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
- package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
- package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
- package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
- package/src/skcapstone/mcp_tools/did_tools.py +11 -8
- package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
- package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
- package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
- package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
- package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
- package/src/skcapstone/mdns_discovery.py +2 -2
- package/src/skcapstone/memory_curator.py +1 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/metrics.py +30 -16
- package/src/skcapstone/migrate_memories.py +4 -3
- package/src/skcapstone/migrate_multi_agent.py +8 -7
- package/src/skcapstone/models.py +47 -5
- package/src/skcapstone/notifications.py +42 -18
- package/src/skcapstone/onboard.py +875 -121
- package/src/skcapstone/operator_link.py +170 -0
- package/src/skcapstone/peer_directory.py +4 -4
- package/src/skcapstone/peers.py +19 -19
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +191 -0
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/preflight.py +3 -3
- package/src/skcapstone/providers/docker.py +28 -28
- package/src/skcapstone/register.py +6 -6
- package/src/skcapstone/registry_client.py +5 -4
- package/src/skcapstone/runtime.py +14 -3
- package/src/skcapstone/scheduled_tasks.py +254 -19
- package/src/skcapstone/scheduler_jobs.py +456 -0
- package/src/skcapstone/scheduler_runner.py +239 -0
- package/src/skcapstone/scheduler_state.py +162 -0
- package/src/skcapstone/sdk.py +310 -0
- package/src/skcapstone/service_health.py +279 -39
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/session_capture.py +1 -1
- package/src/skcapstone/shell.py +7 -1
- package/src/skcapstone/soul.py +3 -1
- package/src/skcapstone/soul_switch.py +3 -1
- package/src/skcapstone/summary.py +6 -6
- package/src/skcapstone/sync_engine.py +15 -15
- package/src/skcapstone/sync_watcher.py +2 -2
- package/src/skcapstone/systemd.py +55 -21
- package/src/skcapstone/team_comms.py +8 -8
- package/src/skcapstone/team_engine.py +1 -1
- package/src/skcapstone/testrunner.py +3 -3
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +15 -6
- package/src/skcapstone/uninstall_wizard.py +11 -3
- package/src/skcapstone/version_check.py +8 -4
- package/src/skcapstone/warmth_anchor.py +4 -2
- package/src/skcapstone/whoami.py +4 -4
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomms-heartbeat.service +21 -0
- package/systemd/skcomms-heartbeat.timer +12 -0
- package/systemd/skcomms-queue-drain.service +17 -0
- package/systemd/skcomms-queue-drain.timer +12 -0
- package/tests/conftest.py +39 -0
- package/tests/integration/test_consciousness_e2e.py +39 -39
- package/tests/test_agent_card.py +1 -1
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_alerts_consumer_topics.py +27 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_chat.py +6 -6
- package/tests/test_claude_md.py +2 -2
- package/tests/test_cli_skills.py +10 -10
- package/tests/test_cli_test_cmd.py +4 -4
- package/tests/test_cli_test_connection.py +1 -1
- package/tests/test_cloud9_bridge.py +6 -6
- package/tests/test_consciousness_e2e.py +1 -1
- package/tests/test_consciousness_loop.py +10 -10
- package/tests/test_coordination.py +25 -0
- package/tests/test_cross_package.py +21 -21
- package/tests/test_daemon.py +4 -4
- package/tests/test_daemon_shutdown.py +1 -1
- package/tests/test_docker_provider.py +29 -29
- package/tests/test_doctor.py +400 -0
- package/tests/test_doctor_skscheduler.py +50 -0
- package/tests/test_dreaming_engine.py +147 -0
- package/tests/test_dreaming_gtd_capture.py +35 -0
- package/tests/test_e2e_automated.py +8 -5
- package/tests/test_fuse_mount.py +10 -10
- package/tests/test_gtd_brief.py +46 -0
- package/tests/test_gtd_malformed_tolerance.py +31 -0
- package/tests/test_housekeeping.py +15 -15
- package/tests/test_identity_migrate.py +251 -0
- package/tests/test_integration_backbone.py +598 -0
- package/tests/test_itil_gtd_lifecycle.py +37 -0
- package/tests/test_jobs_dropins.py +84 -0
- package/tests/test_mcp_server.py +82 -37
- package/tests/test_models.py +48 -4
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_notifications.py +122 -32
- package/tests/test_onboard.py +63 -75
- package/tests/test_operator_link.py +78 -0
- package/tests/test_peers.py +14 -14
- package/tests/test_pillars.py +98 -0
- package/tests/test_preflight.py +3 -3
- package/tests/test_runtime.py +21 -0
- package/tests/test_scheduled_tasks.py +11 -6
- package/tests/test_scheduler_cli.py +47 -0
- package/tests/test_scheduler_features.py +133 -0
- package/tests/test_scheduler_integration.py +87 -0
- package/tests/test_scheduler_jobs.py +155 -0
- package/tests/test_scheduler_runner.py +64 -0
- package/tests/test_scheduler_state.py +57 -0
- package/tests/test_sdk.py +70 -0
- package/tests/test_service_health_incidents.py +34 -0
- package/tests/test_service_registry.py +52 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_snapshots.py +4 -4
- package/tests/test_sync_pipeline.py +26 -26
- package/tests/test_team_comms.py +2 -2
- package/tests/test_testrunner.py +2 -2
- package/tests/test_trust_graph.py +18 -0
- package/tests/test_unified_search.py +2 -2
- package/tests/test_version_check.py +10 -0
- package/tests/test_version_cmd.py +8 -8
- package/tests/test_whoami.py +1 -1
- package/systemd/skcomm-heartbeat.service +0 -18
- package/systemd/skcomm-queue-drain.service +0 -17
- /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
- /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
|
|
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
|
|
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",
|
|
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",
|
|
114
|
-
"
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
|
336
|
-
|
|
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,
|
|
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,
|
|
485
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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,
|
|
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(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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 —
|
|
581
|
-
#
|
|
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
|
-
|
|
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(
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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,
|
|
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
|
-
|
|
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 = {
|
|
1097
|
-
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
1273
|
-
"""Inject
|
|
1318
|
+
def set_skcomms(self, skcomms) -> None:
|
|
1319
|
+
"""Inject SKComms instance for sending responses.
|
|
1274
1320
|
|
|
1275
1321
|
Args:
|
|
1276
|
-
|
|
1322
|
+
skcomms: An initialized SKComms instance.
|
|
1277
1323
|
"""
|
|
1278
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
1485
|
+
if self._config.auto_ack and self._skcomms:
|
|
1437
1486
|
try:
|
|
1438
|
-
self.
|
|
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.
|
|
1510
|
+
if self._skcomms:
|
|
1462
1511
|
try:
|
|
1463
1512
|
from skchat.presence import PresenceIndicator, PresenceState
|
|
1464
|
-
from
|
|
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.
|
|
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,
|
|
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.
|
|
1537
|
+
if self._skcomms:
|
|
1485
1538
|
try:
|
|
1486
1539
|
from skchat.presence import PresenceIndicator, PresenceState
|
|
1487
|
-
from
|
|
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.
|
|
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.
|
|
1577
|
+
if response and self._skcomms:
|
|
1523
1578
|
try:
|
|
1524
|
-
self.
|
|
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,
|
|
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
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1990
|
-
|
|
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
|
-
|
|
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 ""
|