apsimo 1.3.0__py3-none-any.whl

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 (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
apsimo/server.py ADDED
@@ -0,0 +1,4197 @@
1
+ """Colony sidecar FastAPI server.
2
+
3
+ Intelligence sidecar server mounted by agent frameworks (OpenClaw, Hermes,
4
+ etc.) as a plugin via the ``/v1/host`` API surface.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import hashlib
11
+ import json
12
+ import logging
13
+ import math
14
+ import os
15
+ from contextlib import asynccontextmanager
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ # CLI and direct service starts use the same selected private instance.
20
+ from apsimo.util.instance import load_environment
21
+ load_environment()
22
+ from apsimo.environment import apply_environment_aliases
23
+ apply_environment_aliases()
24
+
25
+ from fastapi import FastAPI
26
+
27
+ from apsimo.api.routers.host import (
28
+ router as host_router,
29
+ v2_router as host_v2_router,
30
+ set_llm_router,
31
+ set_autonomy_loop,
32
+ set_scheduler,
33
+ set_chain_manager,
34
+ set_reasoning_loop,
35
+ set_tool_executor,
36
+ set_graph,
37
+ set_consolidator,
38
+ set_response_gate,
39
+ set_signal_collector,
40
+ set_embedder,
41
+ set_reranker,
42
+ set_goals_engine,
43
+ set_contacts_store,
44
+ set_briefings_engine,
45
+ set_world_store,
46
+ set_extraction_pipeline,
47
+ set_metalearner,
48
+ set_research_pipeline,
49
+ set_search_orchestrator,
50
+ set_delivery_bridge,
51
+ set_connection_discoverer,
52
+ set_insight_store,
53
+ set_learner,
54
+ set_skills_registry,
55
+ set_skill_executor,
56
+ set_secrets_manager,
57
+ set_session_store,
58
+ set_task_queue,
59
+ set_commitment_store,
60
+ set_affect_store,
61
+ set_facts_store,
62
+ set_p8_runtime,
63
+ set_context_provenance_store,
64
+ set_response_guard,
65
+ set_engagement_store,
66
+ set_comms_log,
67
+ set_preference_learner,
68
+ set_pattern_store,
69
+ set_surprise_store,
70
+ set_tom_extractor,
71
+ # Multi-Agent v0.7.0
72
+ set_agent_store,
73
+ set_invite_store,
74
+ set_initiative_store,
75
+ set_assignment_engine,
76
+ set_websocket_manager,
77
+ set_telemetry,
78
+ set_session_report_store,
79
+ set_agent_bridge,
80
+ set_initiative_executor,
81
+ set_situation_spine,
82
+ set_cognition_evidence,
83
+ set_drive_governance,
84
+ set_cognition_spine,
85
+ set_cognition_attachment_status,
86
+ set_external_event_intake,
87
+ set_worker_governor,
88
+ supported_capabilities,
89
+ )
90
+
91
+ from apsimo import get_state_dir
92
+
93
+ logger = logging.getLogger(__name__)
94
+
95
+
96
+ def _state_dir() -> Path:
97
+ """Resolve the Colony state directory (wrapper for get_state_dir)."""
98
+ return get_state_dir()
99
+
100
+
101
+ def _embedded_worker_enabled() -> bool:
102
+ """Whether this process may construct its in-process execution worker.
103
+
104
+ Generic installs retain the historical default (enabled). Deployment
105
+ cutovers can pin ``COLONY_EMBEDDED_WORKER_ENABLED=false`` while keeping
106
+ queue maintenance, health, and durable outcome reconciliation online.
107
+ """
108
+
109
+ raw = os.environ.get(
110
+ "COLONY_EMBEDDED_WORKER_ENABLED", "true"
111
+ ).strip().lower()
112
+ if raw in {"1", "true", "yes", "on"}:
113
+ return True
114
+ if raw in {"0", "false", "no", "off"}:
115
+ return False
116
+ raise RuntimeError(
117
+ "COLONY_EMBEDDED_WORKER_ENABLED must be true or false"
118
+ )
119
+
120
+
121
+ def _configured_embedded_worker_enabled() -> bool:
122
+ """Read the same posture for startup wiring outside worker construction.
123
+
124
+ The host's release attestation intentionally reserves
125
+ ``_embedded_worker_enabled`` for the exact lifespan branch that constructs
126
+ ``WorkerNode``. Cognition readiness still needs the same strict setting
127
+ before that branch, so keep this non-construction reader behaviorally
128
+ identical and lock both parsers together in lifecycle tests.
129
+ """
130
+
131
+ raw = os.environ.get(
132
+ "COLONY_EMBEDDED_WORKER_ENABLED", "true"
133
+ ).strip().lower()
134
+ if raw in {"1", "true", "yes", "on"}:
135
+ return True
136
+ if raw in {"0", "false", "no", "off"}:
137
+ return False
138
+ raise RuntimeError(
139
+ "COLONY_EMBEDDED_WORKER_ENABLED must be true or false"
140
+ )
141
+
142
+
143
+ def _resolve_guard_mode(raw: Optional[str]):
144
+ """Strict COLONY_GUARD_MODE parser: 'enforce', 'shadow', or unset only.
145
+
146
+ An unrecognised value (e.g. a typo of 'enforce') must never silently
147
+ fall back to shadow — that would quietly disable the enforcement the
148
+ operator asked for. Log loudly and raise; the ResponseGuard then stays
149
+ uninitialized and the gate endpoints report unavailable (fail closed).
150
+ """
151
+ from apsimo.gate.response_guard import GuardMode
152
+ value = (raw or "").strip().lower()
153
+ if value in ("", "shadow"):
154
+ return GuardMode.SHADOW
155
+ if value == "enforce":
156
+ return GuardMode.ENFORCE
157
+ logger.error(
158
+ "COLONY_GUARD_MODE=%r is not a recognised guard mode "
159
+ "(expected 'enforce' or 'shadow') — refusing to silently "
160
+ "fall back to shadow", raw,
161
+ )
162
+ raise RuntimeError(
163
+ f"COLONY_GUARD_MODE must be 'enforce' or 'shadow', got {raw!r}"
164
+ )
165
+
166
+
167
+ def _cognition_owner_spec(*, router, node_id: str):
168
+ """Build the mechanically isolated embedded ThoughtJobV1 owner."""
169
+
170
+ from apsimo.task_queue.handlers.inference import (
171
+ ThoughtOnlyInferenceHandler,
172
+ )
173
+ from apsimo.task_queue.models import JobType, WorkerCapabilities
174
+ from apsimo.task_queue.routing import THOUGHT_ROUTE
175
+
176
+ handlers = {
177
+ JobType.THOUGHT: ThoughtOnlyInferenceHandler(router),
178
+ }
179
+ capabilities = WorkerCapabilities(
180
+ node_id=node_id,
181
+ capabilities={"cognition_scoped", THOUGHT_ROUTE},
182
+ job_types={JobType.THOUGHT},
183
+ max_concurrent=1,
184
+ )
185
+ return handlers, capabilities
186
+
187
+
188
+ def _cognition_worker_profile(*, configured_mode: str, attached: bool) -> str:
189
+ """Select the embedded worker profile without inspecting partial objects.
190
+
191
+ A configured cognition owner is never allowed to become a generic action
192
+ worker merely because P3 attachment failed. The caller must hold worker
193
+ startup when this returns ``held``.
194
+ """
195
+
196
+ mode = str(configured_mode or "off").strip().lower()
197
+ if mode in {"shadow", "live"}:
198
+ return "thought_only" if attached else "held"
199
+ return "generic"
200
+
201
+
202
+ def _queue_seconds_env(name: str, default: float) -> float:
203
+ try:
204
+ value = float(os.environ.get(name, str(default)))
205
+ except ValueError as exc:
206
+ raise RuntimeError(f"{name} must be numeric") from exc
207
+ if not math.isfinite(value) or value <= 0:
208
+ raise RuntimeError(f"{name} must be positive")
209
+ return value
210
+
211
+
212
+ def _attach_p8_runtime(*, state_dir: Path, facts_store, graph=None):
213
+ """Atomically attach the reviewed P8 stores in explicit shadow mode.
214
+
215
+ Unset, off, unknown, and live all stay dark and create no P8 artifacts.
216
+ Live is intentionally excluded from this integration slice: recipient
217
+ simulation remains an ignored non-real-time shadow observation.
218
+ """
219
+
220
+ from apsimo.tom.integration import P8Runtime, p8_integration_mode
221
+
222
+ set_p8_runtime(None)
223
+ graph_policy = getattr(graph, "set_recall_source_exclusions", None)
224
+ if callable(graph_policy):
225
+ # Clear any policy left on a reused graph before resolving this mode.
226
+ graph_policy(())
227
+ if p8_integration_mode() != "shadow":
228
+ return None
229
+ if facts_store is None:
230
+ raise RuntimeError("P8 shadow requires the canonical SharedFactsStore")
231
+
232
+ visibility = None
233
+ arcs = None
234
+ audit = None
235
+ try:
236
+ from apsimo.tom.arcs import ArcStore
237
+ from apsimo.tom.recipient_audit import (
238
+ open_recipient_simulation_audit_store,
239
+ )
240
+ from apsimo.tom.visibility_store import (
241
+ open_visibility_envelope_store,
242
+ )
243
+
244
+ visibility = open_visibility_envelope_store(
245
+ state_dir / "colony-p8-visibility.db", enabled=True)
246
+ arcs = ArcStore(str(state_dir / "colony-p8-arcs.db"))
247
+ audit = open_recipient_simulation_audit_store(
248
+ state_dir / "colony-p8-recipient-audit.db", mode="shadow")
249
+ if visibility is None or audit is None:
250
+ raise RuntimeError("P8 shadow stores failed to open")
251
+ runtime = P8Runtime(
252
+ visibility_store=visibility,
253
+ arc_store=arcs,
254
+ audit_store=audit,
255
+ facts_store=facts_store,
256
+ mode="shadow",
257
+ )
258
+ if graph is not None:
259
+ if not callable(graph_policy):
260
+ raise RuntimeError(
261
+ "P8 shadow requires graph-wide recall source exclusions")
262
+ # SharedFacts graph rows are compatibility mirrors, not an
263
+ # authorized content path. Typed projection is their only reader.
264
+ graph_policy(
265
+ ("tom:shared_fact",),
266
+ legacy_metadata_markers=("shared_fact",),
267
+ )
268
+ set_p8_runtime(runtime)
269
+ return runtime
270
+ except Exception:
271
+ if callable(graph_policy):
272
+ try:
273
+ graph_policy(())
274
+ except Exception:
275
+ pass
276
+ for store in (audit, arcs, visibility):
277
+ if store is not None:
278
+ try:
279
+ store.close()
280
+ except Exception:
281
+ pass
282
+ set_p8_runtime(None)
283
+ raise
284
+
285
+
286
+ def _build_research_pipeline(*, graph, p8_runtime):
287
+ """Preserve legacy research ownership unless P8 needs the governed graph."""
288
+
289
+ from apsimo.research.pipeline import ResearchPipeline
290
+
291
+ if p8_runtime is None:
292
+ return ResearchPipeline()
293
+ return ResearchPipeline(
294
+ graph=graph,
295
+ allow_fallback_graph=False,
296
+ )
297
+
298
+
299
+ async def _governed_autonomy_stop_signal(autonomy_loop) -> None:
300
+ """Request a prompt stop without awaiting the host route's task join."""
301
+
302
+ if autonomy_loop is not None:
303
+ await autonomy_loop.stop()
304
+
305
+
306
+ def _work_order_runtime_hold_reason(job, project_store, concern_store) -> str:
307
+ """Resolve one canonical cognition WorkOrder to its current source hold."""
308
+
309
+ from collections.abc import Mapping
310
+ from apsimo.self_model.event_concerns import (
311
+ project_turn_concern_hold_reason,
312
+ )
313
+ from apsimo.task_queue.models import JobType
314
+ from apsimo.work_orders import WorkOrderV1
315
+
316
+ payload = getattr(job, "payload", None)
317
+ if not isinstance(payload, Mapping) or payload.get("schema") != "WorkOrderV1":
318
+ return ""
319
+ try:
320
+ order = WorkOrderV1.from_payload(payload)
321
+ except Exception:
322
+ # A malformed row is not a canonical WorkOrder. Fail closed only
323
+ # when it asserts the cognition-spine source this fence owns; generic
324
+ # and external queue jobs retain their existing lifecycle.
325
+ if str(payload.get("source") or "") == "cognition_spine":
326
+ raise
327
+ return ""
328
+ if order.source != "cognition_spine":
329
+ return ""
330
+ if (
331
+ getattr(job, "job_type", None) is not JobType.AGENT_ACTION
332
+ or str(getattr(job, "job_id", "") or "") != order.work_order_id
333
+ ):
334
+ raise RuntimeError("canonical cognition WorkOrder queue identity mismatch")
335
+ ledger = project_store.get_work_order(order.work_order_id)
336
+ if ledger is None:
337
+ raise RuntimeError("canonical cognition WorkOrder ledger row unavailable")
338
+ ledger_order = WorkOrderV1.from_payload(ledger.get("payload") or {})
339
+ if (
340
+ ledger_order.work_order_digest != order.work_order_digest
341
+ or str(ledger.get("work_order_digest") or "")
342
+ != order.work_order_digest
343
+ or str(ledger.get("project_id") or "") != order.project_id
344
+ or str(ledger.get("step_id") or "") != order.step_id
345
+ ):
346
+ raise RuntimeError("canonical cognition WorkOrder ledger binding mismatch")
347
+ project = project_store.get_project(order.project_id)
348
+ if project is None or str(project.source or "") != order.source:
349
+ raise RuntimeError("canonical cognition WorkOrder project binding mismatch")
350
+ return project_turn_concern_hold_reason(project, concern_store)
351
+
352
+
353
+ _COGNITION_RUNTIME_INITIALIZATION_HOLD = (
354
+ "cognition_runtime_initialization_pending"
355
+ )
356
+
357
+
358
+ def _cognition_work_order_startup_hold_reason(job) -> str:
359
+ """Hold only cognition WorkOrders until their durable stores are wired.
360
+
361
+ This deliberately relies only on the queue payload declaration. The
362
+ digest-bound verifier cannot run until ProjectStore initialization has
363
+ succeeded, so a payload asserting the owned source must remain held even
364
+ when the rest of that payload is malformed.
365
+ """
366
+
367
+ from collections.abc import Mapping
368
+
369
+ payload = getattr(job, "payload", None)
370
+ if not isinstance(payload, Mapping):
371
+ return ""
372
+ if (
373
+ payload.get("schema") == "WorkOrderV1"
374
+ and str(payload.get("source") or "") == "cognition_spine"
375
+ ):
376
+ return _COGNITION_RUNTIME_INITIALIZATION_HOLD
377
+ return ""
378
+
379
+
380
+ def _install_cognition_work_order_startup_fence(task_queue) -> None:
381
+ """Install the pre-initialization queue fence when a queue is present."""
382
+
383
+ if task_queue is not None:
384
+ task_queue.queue.configure_runtime_claim_hold(
385
+ _cognition_work_order_startup_hold_reason,
386
+ )
387
+
388
+
389
+ def _install_cognition_work_order_runtime_fence(
390
+ task_queue, project_store, concern_store,
391
+ ) -> None:
392
+ """Replace the startup fence with the complete digest-bound verifier."""
393
+
394
+ if task_queue is None:
395
+ return
396
+
397
+ def _runtime_hold(job) -> str:
398
+ return _work_order_runtime_hold_reason(
399
+ job, project_store, concern_store,
400
+ )
401
+
402
+ task_queue.queue.configure_runtime_claim_hold(_runtime_hold)
403
+
404
+
405
+ def _attach_cognition_spine(
406
+ *,
407
+ state_dir: Path,
408
+ task_queue,
409
+ workspace,
410
+ concern_store,
411
+ project_store,
412
+ project_engine,
413
+ directive_manager,
414
+ llm_router=None,
415
+ embedded_worker_enabled: Optional[bool] = None,
416
+ proposal_store=None,
417
+ ):
418
+ """Attach the typed P3 cognition spine when its migration flag is on.
419
+
420
+ This is kept as one small, directly testable startup seam. In particular,
421
+ the default-off path must not create a database, and an enabled spine must
422
+ never fall back to a partial attachment when a durable dependency is
423
+ absent.
424
+ """
425
+
426
+ from apsimo.cognition.goal_spine import (
427
+ CognitionSpine,
428
+ CognitionSpineStore,
429
+ ThoughtQueueAdapter,
430
+ ThoughtProposalPresentationSink,
431
+ cognition_spine_enabled,
432
+ cognition_spine_mode,
433
+ )
434
+ from apsimo.cognition.runtime import CognitionRuntimeContractV1
435
+
436
+ configured_mode = cognition_spine_mode()
437
+ configured_catalog = (
438
+ ["thought"] if configured_mode in {"shadow", "live"} else []
439
+ )
440
+ set_cognition_spine(None, attachment_status={
441
+ "configured_mode": configured_mode,
442
+ "state": "attaching" if cognition_spine_enabled() else "off",
443
+ "reason": (
444
+ "attachment_in_progress" if cognition_spine_enabled()
445
+ else "cognition_not_configured"
446
+ ),
447
+ "configured_handler_catalog": configured_catalog,
448
+ "effective_handler_catalog": [],
449
+ })
450
+ if not cognition_spine_enabled():
451
+ return None
452
+
453
+ required = {
454
+ "task_queue": task_queue,
455
+ "workspace": workspace,
456
+ "concern_store": concern_store,
457
+ "project_store": project_store,
458
+ "project_engine": project_engine,
459
+ "directive_manager": directive_manager,
460
+ }
461
+ missing = sorted(name for name, value in required.items() if value is None)
462
+ if missing:
463
+ raise RuntimeError(
464
+ "P3 cognition spine missing durable dependencies: "
465
+ + ", ".join(missing)
466
+ )
467
+
468
+ if cognition_spine_mode() == "live":
469
+ from apsimo.directives import Action
470
+ from apsimo.projects.models import projects_mode
471
+
472
+ if projects_mode() != "live":
473
+ raise RuntimeError(
474
+ "P3 live cognition requires ProjectEngine live mode"
475
+ )
476
+ if getattr(project_engine, "_work_orders", None) is None:
477
+ raise RuntimeError(
478
+ "P3 live cognition requires the canonical WorkOrder adapter"
479
+ )
480
+ try:
481
+ # This is a read-only health probe. An active boundary (including
482
+ # an observation blackout) may deny it; either verdict proves the
483
+ # store was readable. Only an exception or malformed result is
484
+ # unhealthy. A read avoids manufacturing a global-pause action
485
+ # block during startup.
486
+ boundary_probe = directive_manager.check(Action(
487
+ kind="read",
488
+ text="colony cognition startup boundary health probe",
489
+ target="cognition startup",
490
+ high_risk=False,
491
+ ))
492
+ except Exception as exc:
493
+ raise RuntimeError(
494
+ "P3 live cognition requires a readable DirectiveGuard"
495
+ ) from exc
496
+ if not isinstance(getattr(boundary_probe, "allowed", None), bool):
497
+ raise RuntimeError(
498
+ "P3 live cognition received a malformed DirectiveGuard verdict"
499
+ )
500
+ if llm_router is None:
501
+ raise RuntimeError(
502
+ "P3 live cognition requires the ThoughtJobV1 LLM router"
503
+ )
504
+ enabled = (
505
+ _configured_embedded_worker_enabled()
506
+ if embedded_worker_enabled is None
507
+ else bool(embedded_worker_enabled)
508
+ )
509
+ if not enabled:
510
+ raise RuntimeError(
511
+ "P3 live cognition requires the embedded strict ThoughtJobV1 "
512
+ "handler"
513
+ )
514
+ configured_owner = os.environ.get(
515
+ "COLONY_THOUGHT_WORKER_NODE_ID", ""
516
+ ).strip()
517
+ if not configured_owner:
518
+ raise RuntimeError(
519
+ "P3 live cognition requires COLONY_THOUGHT_WORKER_NODE_ID"
520
+ )
521
+ from apsimo.chain.node import get_or_create_node_id
522
+
523
+ actual_owner = get_or_create_node_id(state_dir)
524
+ if configured_owner != actual_owner:
525
+ raise RuntimeError(
526
+ "COLONY_THOUGHT_WORKER_NODE_ID does not match the local "
527
+ "cognition worker node"
528
+ )
529
+
530
+ try:
531
+ project_limit = int(
532
+ os.environ.get("COLONY_PROJECTS_MAX_CONCURRENT", "3")
533
+ )
534
+ except ValueError as exc:
535
+ raise RuntimeError(
536
+ "COLONY_PROJECTS_MAX_CONCURRENT must be an integer"
537
+ ) from exc
538
+ if not 1 <= project_limit <= 100:
539
+ raise RuntimeError(
540
+ "COLONY_PROJECTS_MAX_CONCURRENT must be between 1 and 100"
541
+ )
542
+
543
+ available_capabilities = {
544
+ value.strip()
545
+ for value in os.environ.get(
546
+ "COLONY_COGNITION_AVAILABLE_CAPABILITIES",
547
+ "memory:read,reasoning,web:read",
548
+ ).split(",")
549
+ if value.strip()
550
+ }
551
+
552
+ def _charter(proposal, concern):
553
+ # Parsing already enforces the typed schema. This deployment seam
554
+ # adds a deterministic minimum: a goal needs an objective and cited
555
+ # evidence from its originating concern.
556
+ allowed = bool(proposal.objective and proposal.evidence_refs)
557
+ return allowed, (
558
+ "typed_goal_with_source_evidence"
559
+ if allowed
560
+ else "typed_goal_missing_objective_or_source_evidence"
561
+ )
562
+
563
+ def _situation(proposal, concern):
564
+ active = project_engine.open_capacity_used()
565
+ allowed = active < project_limit
566
+ return allowed, (
567
+ "capacity_available" if allowed else "project_capacity_exhausted"
568
+ )
569
+
570
+ runtime_bindings = {
571
+ "charter_store": None,
572
+ "situation_status": None,
573
+ }
574
+
575
+ def _runtime_contract():
576
+ from apsimo.cognition.drive_governance import (
577
+ drive_governance_mode,
578
+ )
579
+ from apsimo.self_model.event_concerns import event_concern_mode
580
+ from apsimo.self_model.workspace import workspace_mode
581
+
582
+ active_id = None
583
+ blockers = []
584
+ charter_store = runtime_bindings["charter_store"]
585
+ if drive_governance_mode() == "live" and charter_store is not None:
586
+ try:
587
+ active = charter_store.active_revision("default")
588
+ active_id = active.revision_id if active is not None else None
589
+ except Exception:
590
+ blockers.append("active_charter_read_failed")
591
+ return CognitionRuntimeContractV1.compose(
592
+ requested_mode=cognition_spine_mode(),
593
+ workspace_mode=workspace_mode(),
594
+ event_concern_mode=event_concern_mode(),
595
+ drive_governance_mode=drive_governance_mode(),
596
+ charter_revision_id=active_id,
597
+ charter_store_attached=charter_store is not None,
598
+ attachment_blockers=tuple(blockers),
599
+ )
600
+
601
+ def _revision_snapshot():
602
+ try:
603
+ boundary_payload = directive_manager.context_brief() or ""
604
+ except Exception:
605
+ boundary_payload = "directive-store-unavailable"
606
+ policy_payload = {
607
+ "goal_admission_policy": "v1",
608
+ "available_capabilities": sorted(available_capabilities),
609
+ }
610
+ try:
611
+ capacity_payload = {
612
+ "planning": project_store.count("planning"),
613
+ "active": project_store.count("active"),
614
+ "used": project_engine.open_capacity_used(),
615
+ "limit": project_limit,
616
+ }
617
+ except Exception:
618
+ capacity_payload = {"status": "capacity-store-unavailable"}
619
+ situation_provider = runtime_bindings["situation_status"]
620
+ if callable(situation_provider):
621
+ try:
622
+ governed_situation = situation_provider()
623
+ except Exception:
624
+ governed_situation = {"status": "unavailable"}
625
+ else:
626
+ governed_situation = {"status": "p6-not-attached"}
627
+ situation_payload = {
628
+ "capacity": capacity_payload,
629
+ "governed_situation": governed_situation,
630
+ }
631
+
632
+ def _revision(label, payload):
633
+ encoded = json.dumps(
634
+ payload, sort_keys=True, separators=(",", ":"), default=str,
635
+ ).encode("utf-8")
636
+ return f"{label}:{hashlib.sha256(encoded).hexdigest()[:24]}"
637
+
638
+ return {
639
+ "policy_revision": _revision("policy", policy_payload),
640
+ "situation_revision": _revision("situation", situation_payload),
641
+ "boundary_revision": _revision("boundary", boundary_payload),
642
+ }
643
+
644
+ cognition_store = CognitionSpineStore(
645
+ str(state_dir / "colony-cognition.db")
646
+ )
647
+ spine = CognitionSpine(
648
+ concern_store=concern_store,
649
+ cognition_store=cognition_store,
650
+ project_engine=project_engine,
651
+ thought_queue=ThoughtQueueAdapter(
652
+ task_queue, cognition_store=cognition_store
653
+ ),
654
+ directive_manager=directive_manager,
655
+ charter_validator=_charter,
656
+ situation_validator=_situation,
657
+ available_capabilities=available_capabilities,
658
+ enforce_runtime_contract=True,
659
+ runtime_contract_provider=_runtime_contract,
660
+ revision_provider=_revision_snapshot,
661
+ worker_health_provider=(
662
+ task_queue.queue.execution_readiness
663
+ if callable(getattr(task_queue.queue, "execution_readiness", None))
664
+ else None
665
+ ),
666
+ proposal_presentation_sink=(
667
+ ThoughtProposalPresentationSink(proposal_store)
668
+ if proposal_store is not None else None
669
+ ),
670
+ owner_person_id=(
671
+ os.environ.get("COLONY_OWNER_PERSON_ID", "").strip()
672
+ or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
673
+ or "owner"
674
+ ),
675
+ )
676
+ spine._runtime_bindings = runtime_bindings
677
+ workspace.cognition_spine = spine
678
+ set_cognition_spine(spine, attachment_status={
679
+ "configured_mode": configured_mode,
680
+ "state": "attached",
681
+ "reason": "cognition_spine_attached_worker_pending",
682
+ "configured_handler_catalog": configured_catalog,
683
+ "effective_handler_catalog": [],
684
+ })
685
+ return spine
686
+
687
+
688
+ def _validator_allowed(result) -> bool:
689
+ """Interpret P3's existing validator shapes without changing its verdict."""
690
+
691
+ if isinstance(result, dict):
692
+ return result.get("allowed") is True
693
+ if isinstance(result, (tuple, list)):
694
+ return bool(result and result[0] is True)
695
+ return result is True
696
+
697
+
698
+ def _situation_failure(reason: str) -> dict:
699
+ """Stable fail-closed result consumed by P3's policy normalizer."""
700
+
701
+ return {
702
+ "allowed": False,
703
+ "reason": reason,
704
+ "evidence_refs": [],
705
+ "does_not_grant_authority": True,
706
+ }
707
+
708
+
709
+ def _capacity_plus_attachment_failure(original_validator, reason: str):
710
+ """Preserve an existing P3 denial but never allow past failed P6 startup."""
711
+
712
+ def _validator(proposal, concern):
713
+ try:
714
+ capacity = original_validator(proposal, concern)
715
+ except Exception:
716
+ logger.exception("P3 capacity validator failed during P6 failure")
717
+ return _situation_failure("capacity_validator_failed_closed")
718
+ if not _validator_allowed(capacity):
719
+ return capacity
720
+ return _situation_failure(reason)
721
+
722
+ return _validator
723
+
724
+
725
+ def _attach_situation_spine(
726
+ *,
727
+ state_dir: Path,
728
+ cognition_spine,
729
+ scheduler=None,
730
+ task_queue=None,
731
+ ):
732
+ """Attach P6 as an observer in shadow and a composed P3 gate in live.
733
+
734
+ The off path clears stale in-process router handles before returning and
735
+ constructs nothing. Shadow deliberately does not replace P3's situation
736
+ validator. Live preserves the exact capacity denial and consults P6 only
737
+ after capacity allows; any reducer, snapshot, or gate failure is a denial.
738
+ """
739
+
740
+ from apsimo.self_model.situation import (
741
+ AppropriatenessGate,
742
+ SituationReducer,
743
+ SituationStore,
744
+ situation_spine_enabled,
745
+ situation_spine_mode,
746
+ task_queue_resource_observation,
747
+ )
748
+
749
+ set_situation_spine(None, None)
750
+ if not situation_spine_enabled():
751
+ return None
752
+
753
+ mode = situation_spine_mode()
754
+ if mode == "live" and cognition_spine is None:
755
+ raise RuntimeError("P6 live situation spine requires the P3 cognition spine")
756
+ original_validator = (
757
+ getattr(cognition_spine, "_situation", None)
758
+ if mode == "live" else None
759
+ )
760
+ if mode == "live" and not callable(original_validator):
761
+ raise RuntimeError(
762
+ "P6 live situation spine requires P3's capacity validator"
763
+ )
764
+ queue_resource = getattr(task_queue, "queue", None)
765
+ if queue_resource is not None and (
766
+ not callable(getattr(queue_resource, "execution_readiness", None))
767
+ or not callable(getattr(queue_resource, "get_queue_stats", None))
768
+ ):
769
+ raise RuntimeError(
770
+ "P6 task queue observation requires readiness and worker truth"
771
+ )
772
+
773
+ store = SituationStore(str(state_dir / "colony-situation.db"))
774
+ try:
775
+ reducer = SituationReducer(store)
776
+ gate = AppropriatenessGate()
777
+ try:
778
+ initial_status = reducer.run_once(limit=100)
779
+ except Exception as exc: # the live wrapper remains explicitly fail closed
780
+ initial_status = {
781
+ "enabled": True,
782
+ "mode": mode,
783
+ "processed": 0,
784
+ "error": f"initial_reduce_failed:{type(exc).__name__}",
785
+ }
786
+
787
+ if scheduler is not None:
788
+ try:
789
+ interval = int(
790
+ os.environ.get(
791
+ "COLONY_SITUATION_REDUCE_INTERVAL_SECONDS", "30"
792
+ )
793
+ )
794
+ except ValueError:
795
+ interval = 30
796
+ interval = max(5, min(3600, interval))
797
+
798
+ if queue_resource is None:
799
+
800
+ def _reduce_situation():
801
+ return reducer.run_once(limit=100)
802
+
803
+ else:
804
+
805
+ async def _reduce_situation():
806
+ observation = await task_queue_resource_observation(
807
+ queue_resource,
808
+ )
809
+ observed = store.ingest(observation)
810
+ result = dict(reducer.run_once(limit=100))
811
+ result["resource_observation"] = {
812
+ "disposition": observed["disposition"],
813
+ "observation_id": observation.observation_id,
814
+ "state": observation.state,
815
+ }
816
+ return result
817
+
818
+ scheduler.register(
819
+ "situation_reduce",
820
+ _reduce_situation,
821
+ interval_seconds=interval,
822
+ metadata={
823
+ "description": (
824
+ "Reduce durable evidence into the scoped P6 situation spine"
825
+ ),
826
+ "mode": mode,
827
+ },
828
+ )
829
+ except Exception:
830
+ store.close()
831
+ raise
832
+
833
+ if mode == "live":
834
+
835
+ def _capacity_plus_situation(proposal, concern):
836
+ try:
837
+ capacity = original_validator(proposal, concern)
838
+ except Exception:
839
+ logger.exception("P3 capacity validator failed before P6")
840
+ return _situation_failure("capacity_validator_failed_closed")
841
+ if not _validator_allowed(capacity):
842
+ return capacity
843
+ try:
844
+ reduced = reducer.run_once(limit=100)
845
+ if not isinstance(reduced, dict) or reduced.get("error"):
846
+ return _situation_failure("situation_reducer_unhealthy")
847
+ snapshot = store.snapshot(
848
+ subject_person_id=proposal.subject_person_id,
849
+ viewer_scope=proposal.viewer_scope,
850
+ )
851
+ verdict = gate.for_goal_proposal(proposal, concern, snapshot)
852
+ if not isinstance(verdict, dict):
853
+ return _situation_failure("situation_gate_invalid_result")
854
+ return verdict
855
+ except Exception:
856
+ logger.exception("P6 live gate failed closed")
857
+ return _situation_failure("situation_gate_failed_closed")
858
+
859
+ cognition_spine._situation = _capacity_plus_situation
860
+
861
+ runtime_bindings = getattr(cognition_spine, "_runtime_bindings", None)
862
+ if isinstance(runtime_bindings, dict):
863
+ runtime_bindings["situation_status"] = reducer.status
864
+
865
+ set_situation_spine(store, reducer)
866
+ return {
867
+ "mode": mode,
868
+ "store": store,
869
+ "reducer": reducer,
870
+ "gate": gate,
871
+ "initial_status": initial_status,
872
+ "original_validator": original_validator,
873
+ }
874
+
875
+
876
+ def _attach_cognition_evidence(
877
+ *,
878
+ state_dir: Path,
879
+ project_store,
880
+ self_model=None,
881
+ expectations=None,
882
+ scheduler=None,
883
+ ):
884
+ """Attach the project outbox and optional receipt-derived learning loop.
885
+
886
+ Project events are operational truth, so their journal projector remains
887
+ active even when learning is off. Off mode keeps only a minimal durable
888
+ passthrough cursor so outcomes already counted by the legacy writer cannot
889
+ be replayed as new learning after live is re-enabled. Live mode requires
890
+ the canonical SelfModel and fails closed on an unhealthy initial
891
+ projection/reduction instead of falling back to self-reported success.
892
+ """
893
+
894
+ from apsimo.cognition.evidence_pipeline import (
895
+ CognitionEvidenceReducer,
896
+ CognitionEvidenceStore,
897
+ cognition_evidence_mode,
898
+ )
899
+ from apsimo.projects.event_outbox import ProjectEventProjector
900
+
901
+ mode = cognition_evidence_mode()
902
+ set_cognition_evidence(None, None, None, {
903
+ "configured_mode": mode,
904
+ "state": "attaching" if project_store is not None else "off",
905
+ "reason": (
906
+ "attachment_in_progress" if project_store is not None
907
+ else "project_store_unavailable"
908
+ ),
909
+ })
910
+ if project_store is None:
911
+ if mode == "live":
912
+ raise RuntimeError(
913
+ "live cognition evidence requires the canonical ProjectStore"
914
+ )
915
+ return None
916
+ if mode == "live" and self_model is None:
917
+ raise RuntimeError(
918
+ "live cognition evidence requires the canonical SelfModel"
919
+ )
920
+ if mode == "live" and scheduler is None:
921
+ raise RuntimeError(
922
+ "live cognition evidence requires the autonomy scheduler"
923
+ )
924
+
925
+ projector = ProjectEventProjector(project_store)
926
+ evidence_store = None
927
+ reducer = None
928
+ try:
929
+ initial_outbox = projector.run_once(limit=100)
930
+ if mode == "live" and (
931
+ initial_outbox.get("failed")
932
+ or initial_outbox.get("acknowledgement_failures")
933
+ or initial_outbox.get("outbox", {}).get("last_error")
934
+ ):
935
+ raise RuntimeError(
936
+ "live cognition evidence project outbox is unhealthy"
937
+ )
938
+
939
+ try:
940
+ interval = int(os.environ.get(
941
+ "COLONY_COGNITION_EVIDENCE_INTERVAL_SECONDS", "30",
942
+ ))
943
+ except ValueError as exc:
944
+ raise RuntimeError(
945
+ "COLONY_COGNITION_EVIDENCE_INTERVAL_SECONDS must be an integer"
946
+ ) from exc
947
+ interval = max(5, min(3600, interval))
948
+
949
+ evidence_store = CognitionEvidenceStore(
950
+ str(state_dir / "colony-cognition-evidence.db")
951
+ )
952
+ reducer = CognitionEvidenceReducer(
953
+ evidence_store,
954
+ project_store=project_store,
955
+ self_model=self_model,
956
+ expectations=expectations,
957
+ project_event_projector=projector,
958
+ )
959
+ attachment_armed = {"value": False}
960
+ if scheduler is not None:
961
+
962
+ def _reduce_cognition_evidence():
963
+ if not attachment_armed["value"]:
964
+ return {
965
+ "enabled": mode != "off", "mode": mode,
966
+ "processed": 0, "error": "attachment_not_armed",
967
+ }
968
+ return reducer.run_once(limit=100)
969
+
970
+ # Register before any live sink can mutate. A registration failure
971
+ # therefore leaves competence, expectations, and the evidence
972
+ # cursor untouched. If a scheduler ticks concurrently during
973
+ # attachment, the captured callback remains inert until armed.
974
+ scheduler.register(
975
+ "cognition_evidence_reduce",
976
+ _reduce_cognition_evidence,
977
+ interval_seconds=interval,
978
+ metadata={
979
+ "description": (
980
+ "Checkpoint or reduce receipt-bound project evidence"
981
+ ),
982
+ "mode": mode,
983
+ },
984
+ )
985
+ initial_status = reducer.run_once(limit=100)
986
+ if mode == "live" and initial_status.get("error"):
987
+ raise RuntimeError(
988
+ "live cognition evidence initial reduction failed: "
989
+ + str(initial_status["error"])[:300]
990
+ )
991
+ attachment = {
992
+ "configured_mode": mode,
993
+ "state": "attached" if scheduler is not None else "degraded",
994
+ "reason": (
995
+ "evidence_passthrough_cursor_attached_learning_off"
996
+ if mode == "off" and scheduler is not None else
997
+ "evidence_passthrough_cursor_restart_only"
998
+ if mode == "off" else
999
+ "receipt_derived_evidence_attached"
1000
+ if scheduler is not None else
1001
+ "receipt_derived_evidence_restart_replay_only"
1002
+ ),
1003
+ }
1004
+ set_cognition_evidence(
1005
+ evidence_store, reducer, projector, attachment,
1006
+ )
1007
+ attachment_armed["value"] = True
1008
+ return {
1009
+ "mode": mode,
1010
+ "store": evidence_store,
1011
+ "reducer": reducer,
1012
+ "projector": projector,
1013
+ "initial_status": initial_status,
1014
+ "attachment": attachment,
1015
+ }
1016
+ except Exception:
1017
+ if evidence_store is not None:
1018
+ evidence_store.close()
1019
+ set_cognition_evidence(None, None, None, {
1020
+ "configured_mode": mode,
1021
+ "state": "failed",
1022
+ "reason": "cognition_evidence_attachment_failed",
1023
+ })
1024
+ raise
1025
+
1026
+
1027
+ def _compose_p7_charter_admission(
1028
+ original_validator, charter_store, directive_manager=None,
1029
+ ):
1030
+ """Narrow P3 admission to the active owner-ratified P7 charter.
1031
+
1032
+ P7 lifecycle activation is already bound to the canonical approval
1033
+ authority. This adapter consumes that durable fact; it never creates a
1034
+ second approval path and never treats charter prose as executable policy.
1035
+ """
1036
+
1037
+ from apsimo.cognition.drive_governance import (
1038
+ CharterAdmissionConstraintsV1,
1039
+ ScopeV1,
1040
+ )
1041
+
1042
+ def _parts(result):
1043
+ if isinstance(result, dict):
1044
+ return (
1045
+ result.get("allowed") is True,
1046
+ str(result.get("reason") or "charter_validator_denied")[:500],
1047
+ [str(ref) for ref in result.get("evidence_refs") or ()],
1048
+ )
1049
+ if isinstance(result, (tuple, list)):
1050
+ return (
1051
+ bool(result and result[0] is True),
1052
+ str(result[1] if len(result) > 1 else "charter_validator_denied")[:500],
1053
+ [str(ref) for ref in (result[2] if len(result) > 2 else ())],
1054
+ )
1055
+ return result is True, "charter_validator_denied", []
1056
+
1057
+ def _validator(proposal, concern):
1058
+ try:
1059
+ base = original_validator(proposal, concern)
1060
+ except Exception:
1061
+ return {
1062
+ "allowed": False,
1063
+ "reason": "base_charter_validator_failed",
1064
+ "evidence_refs": [],
1065
+ }
1066
+ allowed, _reason, evidence = _parts(base)
1067
+ if not allowed:
1068
+ return base
1069
+ try:
1070
+ active = charter_store.active_revision("default")
1071
+ except Exception:
1072
+ return {
1073
+ "allowed": False,
1074
+ "reason": "active_charter_read_failed",
1075
+ "evidence_refs": list(dict.fromkeys(evidence)),
1076
+ }
1077
+ if active is None:
1078
+ return {
1079
+ "allowed": False,
1080
+ "reason": "active_owner_ratified_charter_required",
1081
+ "evidence_refs": list(dict.fromkeys(evidence)),
1082
+ }
1083
+ try:
1084
+ proposal_scope = ScopeV1(
1085
+ proposal.subject_person_id,
1086
+ proposal.viewer_scope,
1087
+ proposal.shareability,
1088
+ )
1089
+ except Exception:
1090
+ return {
1091
+ "allowed": False,
1092
+ "reason": "goal_scope_invalid_for_active_charter",
1093
+ "evidence_refs": list(dict.fromkeys(evidence)),
1094
+ }
1095
+ charter_evidence = [
1096
+ active.revision_id,
1097
+ f"charter-active:{active.revision_id}",
1098
+ *active.evidence_refs,
1099
+ ]
1100
+ combined = list(dict.fromkeys([*charter_evidence, *evidence]))[:30]
1101
+ if not active.scope.permits_child(proposal_scope):
1102
+ return {
1103
+ "allowed": False,
1104
+ "reason": "active_charter_scope_holds_goal",
1105
+ "evidence_refs": combined,
1106
+ }
1107
+ constraints = (
1108
+ active.admission_constraints
1109
+ if getattr(active, "admission_constraints", None) is not None
1110
+ else CharterAdmissionConstraintsV1()
1111
+ )
1112
+ required_boundaries = set(constraints.required_boundary_refs)
1113
+ if required_boundaries:
1114
+ if directive_manager is None:
1115
+ return {
1116
+ "allowed": False,
1117
+ "reason": "charter_boundary_reader_unavailable",
1118
+ "evidence_refs": combined,
1119
+ }
1120
+ try:
1121
+ active_boundaries = {
1122
+ str(item.id) for item in directive_manager.active()
1123
+ }
1124
+ except Exception:
1125
+ return {
1126
+ "allowed": False,
1127
+ "reason": "charter_boundary_read_failed",
1128
+ "evidence_refs": combined,
1129
+ }
1130
+ if not required_boundaries.issubset(active_boundaries):
1131
+ return {
1132
+ "allowed": False,
1133
+ "reason": "charter_required_boundary_missing",
1134
+ "evidence_refs": combined,
1135
+ }
1136
+ objective = (
1137
+ f"{getattr(proposal, 'title', '')} "
1138
+ f"{getattr(proposal, 'objective', '')}"
1139
+ ).casefold()
1140
+ if any(term in objective for term in constraints.objective_deny_terms):
1141
+ return {
1142
+ "allowed": False,
1143
+ "reason": "charter_objective_explicitly_denied",
1144
+ "evidence_refs": combined,
1145
+ }
1146
+ if constraints.objective_allow_terms and not any(
1147
+ term in objective for term in constraints.objective_allow_terms
1148
+ ):
1149
+ return {
1150
+ "allowed": False,
1151
+ "reason": "charter_objective_not_explicitly_allowed",
1152
+ "evidence_refs": combined,
1153
+ }
1154
+ destructive_terms = {
1155
+ "delete", "destroy", "drop", "format", "overwrite", "wipe",
1156
+ }
1157
+ if (
1158
+ any(term in objective for term in destructive_terms)
1159
+ and not constraints.allow_destructive
1160
+ ):
1161
+ return {
1162
+ "allowed": False,
1163
+ "reason": "charter_destructive_objective_not_allowed",
1164
+ "evidence_refs": combined,
1165
+ }
1166
+ requested = set(getattr(proposal, "required_capabilities", ()) or ())
1167
+ denied = sorted(requested.intersection(constraints.capability_deny))
1168
+ if denied:
1169
+ return {
1170
+ "allowed": False,
1171
+ "reason": "charter_capability_explicitly_denied:" + ",".join(denied),
1172
+ "evidence_refs": combined,
1173
+ }
1174
+ if not requested.issubset(set(constraints.capability_ceiling)):
1175
+ return {
1176
+ "allowed": False,
1177
+ "reason": "charter_capability_ceiling_exceeded",
1178
+ "evidence_refs": combined,
1179
+ }
1180
+ if "root:shell" in requested and not constraints.allow_root_shell:
1181
+ return {
1182
+ "allowed": False,
1183
+ "reason": "charter_root_shell_not_allowed",
1184
+ "evidence_refs": combined,
1185
+ }
1186
+ if "messaging:send" in requested:
1187
+ if not constraints.allow_messaging:
1188
+ return {
1189
+ "allowed": False,
1190
+ "reason": "charter_messaging_not_allowed",
1191
+ "evidence_refs": combined,
1192
+ }
1193
+ # GoalProposalV1 deliberately has no recipient field. Until a
1194
+ # digest-bound recipient envelope exists, even an explicit
1195
+ # charter allowance cannot infer one from the concern/model.
1196
+ return {
1197
+ "allowed": False,
1198
+ "reason": "charter_recipient_envelope_required",
1199
+ "evidence_refs": combined,
1200
+ }
1201
+ if proposal.shareability not in set(constraints.allowed_shareability):
1202
+ return {
1203
+ "allowed": False,
1204
+ "reason": "charter_shareability_not_allowed",
1205
+ "evidence_refs": combined,
1206
+ }
1207
+ return {
1208
+ "allowed": True,
1209
+ "reason": "active_owner_ratified_charter",
1210
+ "evidence_refs": combined,
1211
+ }
1212
+
1213
+ return _validator
1214
+
1215
+
1216
+ def _attach_drive_governance(
1217
+ *,
1218
+ state_dir: Path,
1219
+ cognition_spine,
1220
+ workspace,
1221
+ project_store,
1222
+ directive_manager,
1223
+ approval_authority=None,
1224
+ ):
1225
+ """Attach P7 only to the complete, durable P3 dependency graph."""
1226
+
1227
+ from apsimo.cognition.drive_governance import (
1228
+ DriveGovernance,
1229
+ DriveGovernanceStore,
1230
+ DriveRanker,
1231
+ drive_governance_mode,
1232
+ )
1233
+ from apsimo.initiatives.approval_authority import (
1234
+ ApprovalAuthorityStore,
1235
+ )
1236
+
1237
+ set_drive_governance(None, None, None)
1238
+ mode = drive_governance_mode()
1239
+ if mode == "off":
1240
+ return None
1241
+
1242
+ required = {
1243
+ "cognition_spine": cognition_spine,
1244
+ "workspace": workspace,
1245
+ "project_store": project_store,
1246
+ "directive_manager": directive_manager,
1247
+ }
1248
+ missing = sorted(name for name, value in required.items() if value is None)
1249
+ cognition_store = getattr(cognition_spine, "store", None)
1250
+ project_engine = getattr(cognition_spine, "project_engine", None)
1251
+ if cognition_store is None:
1252
+ missing.append("cognition_store")
1253
+ if project_engine is None:
1254
+ missing.append("project_engine")
1255
+ elif getattr(project_engine, "store", None) is not project_store:
1256
+ missing.append("shared_project_store")
1257
+ if missing:
1258
+ raise RuntimeError(
1259
+ "P7 drive governance missing durable dependencies: "
1260
+ + ", ".join(sorted(set(missing)))
1261
+ )
1262
+ resolver = getattr(cognition_store, "get_policy_decision", None)
1263
+ if not callable(resolver):
1264
+ raise RuntimeError(
1265
+ "P7 drive governance requires the P3 policy decision resolver"
1266
+ )
1267
+
1268
+ store = DriveGovernanceStore(state_dir / "cognition-drive-governance.db")
1269
+ try:
1270
+ shared_authority = approval_authority
1271
+ if mode in {"bootstrap", "live"} and shared_authority is None:
1272
+ shared_authority = ApprovalAuthorityStore(
1273
+ state_dir / "approval_authority.db"
1274
+ )
1275
+ governance = DriveGovernance(store, shared_authority, mode=mode)
1276
+ ranker = DriveRanker(
1277
+ store,
1278
+ policy_decision_resolver=resolver,
1279
+ directive_manager=directive_manager,
1280
+ )
1281
+ except Exception:
1282
+ store.close()
1283
+ raise
1284
+ original_charter_validator = getattr(cognition_spine, "_charter", None)
1285
+ if mode == "live":
1286
+ if not callable(original_charter_validator):
1287
+ store.close()
1288
+ raise RuntimeError(
1289
+ "P7 live governance requires P3's charter validator"
1290
+ )
1291
+ cognition_spine._charter = _compose_p7_charter_admission(
1292
+ original_charter_validator, store, directive_manager,
1293
+ )
1294
+ runtime_bindings = getattr(cognition_spine, "_runtime_bindings", None)
1295
+ if isinstance(runtime_bindings, dict):
1296
+ runtime_bindings["charter_store"] = store
1297
+ workspace.drive_governance = governance
1298
+ workspace.drive_ranker = ranker
1299
+ set_drive_governance(governance, ranker, project_store)
1300
+ return {
1301
+ "mode": mode,
1302
+ "store": store,
1303
+ "governance": governance,
1304
+ "ranker": ranker,
1305
+ "approval_authority": shared_authority,
1306
+ "original_charter_validator": original_charter_validator,
1307
+ }
1308
+
1309
+
1310
+ def _initialize_controlled_learning(
1311
+ *,
1312
+ state_dir: Path,
1313
+ adaptive_params,
1314
+ journal=None,
1315
+ ) -> dict:
1316
+ """Build the one P4 evidence/experiment graph for this process.
1317
+
1318
+ The migration flags retain their historical meanings: P4 mode defaults to
1319
+ ``off`` (legacy weekly evaluation), while the independent benchmark and
1320
+ experiment feature flags control whether their databases are opened at
1321
+ all. Setters are cleared before construction so a second lifespan in the
1322
+ same interpreter cannot retain stale authority or a stale writer.
1323
+ """
1324
+
1325
+ from apsimo.api.routers.host import (
1326
+ set_benchmark,
1327
+ set_experiments,
1328
+ set_learning_feedback_store,
1329
+ )
1330
+ from apsimo.initiatives.approval_authority import (
1331
+ ApprovalAuthorityStore,
1332
+ )
1333
+ from apsimo.intelligence.learning.feedback_store import (
1334
+ FeedbackStore,
1335
+ )
1336
+ from apsimo.self_model.benchmark import (
1337
+ BenchmarkStore,
1338
+ SelfhoodBenchmark,
1339
+ benchmark_enabled,
1340
+ )
1341
+ from apsimo.self_model.experiments import (
1342
+ ExperimentEngine,
1343
+ ExperimentStore,
1344
+ experiment_pregrants_from_env,
1345
+ experiments_enabled,
1346
+ )
1347
+
1348
+ set_learning_feedback_store(None)
1349
+ set_benchmark(None)
1350
+ set_experiments(None)
1351
+
1352
+ correction_store = FeedbackStore(
1353
+ db_path=str(state_dir / "colony-learning-feedback.db"))
1354
+
1355
+ benchmark = None
1356
+ if benchmark_enabled():
1357
+ benchmark = SelfhoodBenchmark(
1358
+ BenchmarkStore(db_path=str(state_dir / "colony-benchmark.db")),
1359
+ corrections=correction_store,
1360
+ )
1361
+
1362
+ experiments = None
1363
+ approval_authority = None
1364
+ if experiments_enabled():
1365
+ if adaptive_params is None:
1366
+ raise RuntimeError(
1367
+ "P4 experiments require the adaptive parameter store")
1368
+ if benchmark is None:
1369
+ raise RuntimeError(
1370
+ "P4 experiments require the canonical SelfhoodBenchmark")
1371
+ # This is the same approval_authority.db used by the queue approval
1372
+ # routes. Do not create an experiment-specific authority ledger.
1373
+ approval_authority = ApprovalAuthorityStore()
1374
+ experiments = ExperimentEngine(
1375
+ ExperimentStore(
1376
+ db_path=str(state_dir / "colony-experiments.db")),
1377
+ params=adaptive_params,
1378
+ benchmark=benchmark,
1379
+ journal=journal,
1380
+ approval_authority=approval_authority,
1381
+ pregranted_ranges=experiment_pregrants_from_env(),
1382
+ )
1383
+
1384
+ # Publish only after the complete configured graph constructed. A bad
1385
+ # pregrant or missing dependency must not expose a partially wired P4.
1386
+ set_learning_feedback_store(correction_store)
1387
+ set_benchmark(benchmark)
1388
+ set_experiments(experiments)
1389
+
1390
+ return {
1391
+ "corrections": correction_store,
1392
+ "benchmark": benchmark,
1393
+ "experiments": experiments,
1394
+ "approval_authority": approval_authority,
1395
+ }
1396
+
1397
+
1398
+ def _wire_controlled_learning_pipeline(
1399
+ cognition_pipeline,
1400
+ controlled_learning: dict,
1401
+ ) -> None:
1402
+ """Attach P4's detector adapter and correction reader to shared objects."""
1403
+
1404
+ if cognition_pipeline is None:
1405
+ return
1406
+ correction_store = controlled_learning.get("corrections")
1407
+ if correction_store is not None:
1408
+ cognition_pipeline.meta_learner.set_feedback_store(correction_store)
1409
+ experiments = controlled_learning.get("experiments")
1410
+ if experiments is not None:
1411
+ # StrategyAdjuster is proposal-only; ExperimentEngine remains the sole
1412
+ # adaptive-parameter writer and the only component that can start.
1413
+ cognition_pipeline.strategy_adjuster.set_experiment_proposer(
1414
+ experiments)
1415
+
1416
+
1417
+ def _scheduler_health_check(autonomy_loop) -> dict:
1418
+ """Periodic health_check task body (module-level so it is testable).
1419
+
1420
+ Reports the actual wiring state instead of an unconditional
1421
+ ``{"status": "ok"}``. No blanket try/except: an exception here must
1422
+ surface as a scheduler failure receipt, never be swallowed into green.
1423
+ """
1424
+ import apsimo.api.routers.host as _h
1425
+ wired = 0
1426
+ for _n in ("_commitment_store", "_goals_store", "_affect_store",
1427
+ "_contacts_store", "_delivery_bridge", "_workspace",
1428
+ "_metalearner"):
1429
+ if getattr(_h, _n, None) is not None:
1430
+ wired += 1
1431
+ return {"status": "ok" if wired else "degraded",
1432
+ "subsystems_wired": wired,
1433
+ "autonomy_running": bool(getattr(autonomy_loop, "_running", False))}
1434
+
1435
+
1436
+ @asynccontextmanager
1437
+ async def lifespan(app: FastAPI):
1438
+ """Initialize subsystems on startup, tear down on shutdown."""
1439
+ from apsimo.initiatives.approval_authority import (
1440
+ GRANT_MAX_TTL_ENV,
1441
+ GRANT_MAX_USES_ENV,
1442
+ GRANT_UNLIMITED_SENTINEL,
1443
+ resolve_grant_envelope,
1444
+ )
1445
+
1446
+ try:
1447
+ grant_envelope = resolve_grant_envelope()
1448
+ except RuntimeError as exc:
1449
+ logger.error(
1450
+ "INVALID APPROVAL GRANT ENVELOPE — refusing startup: %s", exc,
1451
+ )
1452
+ raise
1453
+ app.state.grant_envelope = grant_envelope
1454
+ if grant_envelope.standing_dimensions:
1455
+ descriptions = []
1456
+ if grant_envelope.max_ttl_seconds is None:
1457
+ descriptions.append(f"{GRANT_MAX_TTL_ENV} (no expiry)")
1458
+ if grant_envelope.max_uses is None:
1459
+ descriptions.append(f"{GRANT_MAX_USES_ENV} (no use cap)")
1460
+ logger.warning(
1461
+ "STANDING APPROVAL AUTHORITY ENABLED — %s configured as %r; "
1462
+ "exact-scope grants persist in those dimensions until revoked",
1463
+ "; ".join(descriptions),
1464
+ GRANT_UNLIMITED_SENTINEL,
1465
+ )
1466
+ else:
1467
+ logger.info(
1468
+ "Approval grant envelope bounded: %s=%s, %s=%s",
1469
+ GRANT_MAX_TTL_ENV,
1470
+ grant_envelope.max_ttl_seconds,
1471
+ GRANT_MAX_USES_ENV,
1472
+ grant_envelope.max_uses,
1473
+ )
1474
+ state_dir = _state_dir()
1475
+ _p8_wiring = None
1476
+ # Clear stale process authority before any queue/worker can be constructed.
1477
+ # A repeated lifespan that later fails remains fail-closed in live mode.
1478
+ set_worker_governor(None)
1479
+
1480
+ # --- Phase C external text/system evidence intake ---
1481
+ _external_event_intake = None
1482
+ set_external_event_intake(None)
1483
+ try:
1484
+ from apsimo.cognition.external_events import (
1485
+ ExternalEventInboxStore,
1486
+ ExternalEventIntake,
1487
+ )
1488
+ _external_event_intake = ExternalEventIntake(
1489
+ ExternalEventInboxStore(
1490
+ str(state_dir / "external-cognition-events.db")
1491
+ )
1492
+ )
1493
+ set_external_event_intake(_external_event_intake)
1494
+ logger.info(
1495
+ "External cognition event intake initialized (text/system only; db=%s)",
1496
+ state_dir / "external-cognition-events.db",
1497
+ )
1498
+ except Exception as exc:
1499
+ logger.error("External cognition event intake failed: %s", exc)
1500
+
1501
+ # --- 0. Adaptive parameters (meta-learning read-back path) ---
1502
+ # Created first so downstream consumers (consolidator, graph recall,
1503
+ # cognition pipeline) can take a handle; the ActionJournal is attached
1504
+ # in the self-model section once it exists.
1505
+ _adaptive_params = None
1506
+ try:
1507
+ from apsimo.self_model.params import (
1508
+ AdaptiveParamStore, register_core_params,
1509
+ )
1510
+ _adaptive_params = AdaptiveParamStore(
1511
+ db_path=str(state_dir / "colony-params.db"))
1512
+ register_core_params(_adaptive_params)
1513
+ try:
1514
+ from apsimo.api.routers.host import set_adaptive_params
1515
+ set_adaptive_params(_adaptive_params)
1516
+ except ImportError:
1517
+ pass
1518
+ logger.info("AdaptiveParamStore initialized (db=%s)",
1519
+ state_dir / "colony-params.db")
1520
+ except Exception as exc:
1521
+ logger.warning("AdaptiveParamStore init failed: %s", exc)
1522
+
1523
+ # --- 1. LLM Router ---
1524
+ llm_router = None
1525
+ try:
1526
+ from apsimo.router.router import LLMRouter
1527
+ from apsimo.router.tiers import build_tiers_from_host
1528
+ import json as _json
1529
+
1530
+ config_path = state_dir / ".colony-llm-config.json"
1531
+ if config_path.exists():
1532
+ try:
1533
+ host_llm_config = _json.loads(config_path.read_text())
1534
+ llm_router = LLMRouter(tiers={})
1535
+ llm_router.configure(host_llm_config, config_path=config_path)
1536
+ logger.info(
1537
+ "LLMRouter initialized from persisted host config (provider=%s)",
1538
+ host_llm_config.get("provider", "unknown"),
1539
+ )
1540
+ except Exception as cfg_exc:
1541
+ logger.warning("Persisted LLM config unavailable: %s", type(cfg_exc).__name__)
1542
+ llm_router = LLMRouter(tiers={})
1543
+ llm_router.watch_config(config_path)
1544
+ else:
1545
+ llm_router = LLMRouter(tiers={})
1546
+ llm_router.watch_config(config_path)
1547
+ logger.info("LLMRouter awaiting explicit host config")
1548
+ except Exception as exc:
1549
+ logger.warning("LLMRouter init failed — reasoning will not be available: %s", exc)
1550
+
1551
+ if llm_router is not None:
1552
+ set_llm_router(llm_router)
1553
+
1554
+ # --- 2. Reasoning loop ---
1555
+ if llm_router is not None:
1556
+ try:
1557
+ from apsimo.reasoning import ReasoningLoop, ToolExecutor
1558
+ tool_executor = ToolExecutor()
1559
+ reasoning_loop = ReasoningLoop(model=llm_router, tools=tool_executor)
1560
+ set_reasoning_loop(reasoning_loop)
1561
+ # Native tools will be registered after search orchestrator is wired
1562
+ set_tool_executor(tool_executor)
1563
+ logger.info("ReasoningLoop initialized")
1564
+ except Exception as exc:
1565
+ logger.warning("ReasoningLoop init failed: %s", exc)
1566
+
1567
+ # --- 3. Neo4j Graph memory ---
1568
+ graph = None
1569
+ if os.environ.get("COLONY_GRAPH_ENABLED", "true").lower() not in {"0", "false", "off"}:
1570
+ try:
1571
+ from apsimo.intelligence.graph.client import ColonyGraph, GraphConfig
1572
+ from pydantic import SecretStr
1573
+ neo4j_uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
1574
+ neo4j_user = os.environ.get("NEO4J_USER", "neo4j")
1575
+ neo4j_pass = os.environ.get("NEO4J_PASSWORD", "")
1576
+ # Neo4j Community Edition only has the "neo4j" database.
1577
+ # Enterprise users can override via NEO4J_DATABASE.
1578
+ neo4j_db = os.environ.get("NEO4J_DATABASE", "neo4j")
1579
+ graph_config = GraphConfig(
1580
+ uri=neo4j_uri,
1581
+ auth=(neo4j_user, SecretStr(neo4j_pass)) if neo4j_pass else None,
1582
+ database=neo4j_db,
1583
+ )
1584
+ graph = ColonyGraph(graph_config)
1585
+ # Apply graph schema constraints/indexes before any queries run
1586
+ try:
1587
+ from apsimo.intelligence.graph.migrations import run_migrations
1588
+ await run_migrations(graph.driver, database=neo4j_db)
1589
+ except Exception as exc:
1590
+ logger.warning("Graph migrations failed (queries may be degraded): %s", exc)
1591
+ set_graph(graph)
1592
+ # Wire graph into ToolExecutor for capability-gap detection
1593
+ try:
1594
+ import apsimo.api.routers.host as _host_router
1595
+ te = _host_router._tool_executor
1596
+ if te is not None:
1597
+ te._graph = graph
1598
+ except Exception:
1599
+ logger.warning("ToolExecutor graph wiring failed (capability-gap detection degraded)")
1600
+ logger.info("ColonyGraph initialized (uri=%s db=%s)", neo4j_uri, neo4j_db)
1601
+
1602
+ # Ensure Colony self-representation in graph (v0.11.0)
1603
+ try:
1604
+ await graph.ensure_colony_self()
1605
+ except Exception as self_exc:
1606
+ logger.warning("Colony self-representation setup skipped: %s", self_exc)
1607
+
1608
+ # Wire consolidator (adaptive merge threshold when params wired)
1609
+ try:
1610
+ from apsimo.intelligence.graph.consolidator import MemoryConsolidator
1611
+ consolidator = MemoryConsolidator(graph, params=_adaptive_params)
1612
+ set_consolidator(consolidator)
1613
+ logger.info("MemoryConsolidator initialized")
1614
+ except Exception as cexc:
1615
+ logger.warning("MemoryConsolidator init skipped: %s", cexc)
1616
+ if _adaptive_params is not None:
1617
+ try:
1618
+ graph.set_adaptive_params(_adaptive_params)
1619
+ except Exception:
1620
+ logger.debug("graph adaptive-params wiring failed", exc_info=True)
1621
+ except Exception as exc:
1622
+ logger.warning("ColonyGraph init failed — memory endpoints will be degraded: %s", exc)
1623
+
1624
+ else:
1625
+ set_graph(None)
1626
+ logger.info("Graph disabled; canonical source memory and SQLite state remain available")
1627
+
1628
+ # --- 4. Response Gate (safety pipeline) ---
1629
+ _gate_ref = None
1630
+ _gate_config = None
1631
+ _gate_audit = None
1632
+ try:
1633
+ from apsimo.gate import ResponseGate, GateConfig
1634
+ from apsimo.gate.audit import InMemoryAuditLog
1635
+ # L7 send-delay (the cancel window) is env-tunable; default 0 = no
1636
+ # hold. Set COLONY_GATE_SEND_DELAY_SECS>0 to enable a real cancel
1637
+ # window on the request-path gate.
1638
+ try:
1639
+ _gate_delay = float(os.environ.get("COLONY_GATE_SEND_DELAY_SECS", "0"))
1640
+ except ValueError:
1641
+ _gate_delay = 0.0
1642
+ gate_config = GateConfig(send_delay_seconds=_gate_delay)
1643
+ gate_audit = InMemoryAuditLog()
1644
+ gate = ResponseGate(gate_config, session_store=None, audit_log=gate_audit)
1645
+ set_response_gate(gate)
1646
+ # Stash refs for re-wiring after session store is available
1647
+ _gate_ref = gate
1648
+ _gate_config = gate_config
1649
+ _gate_audit = gate_audit
1650
+ logger.info("ResponseGate initialized (send_delay=%.1fs, secondary_review=%s)",
1651
+ _gate_delay, getattr(gate_config, "enable_secondary_review", False))
1652
+
1653
+ # Re-wire ResponseGate with session store once available
1654
+ except Exception as exc:
1655
+ logger.warning("ResponseGate init failed — safety checks will pass-through: %s", exc)
1656
+
1657
+ # --- 5. Signal Collector ---
1658
+ signal_collector = None
1659
+ if graph is not None:
1660
+ try:
1661
+ from apsimo.intelligence.mind_model.graph_baseline import GraphBaselineStore
1662
+ from apsimo.intelligence.mind_model.signal_collector import SignalCollector
1663
+ baseline_store = GraphBaselineStore(graph)
1664
+ signal_collector = SignalCollector(baseline_store=baseline_store, graph=graph)
1665
+ set_signal_collector(signal_collector)
1666
+ logger.info("SignalCollector initialized (GraphBaselineStore backed by Neo4j)")
1667
+ except Exception as exc:
1668
+ logger.warning("SignalCollector init failed: %s", exc)
1669
+ else:
1670
+ logger.warning("SignalCollector skipped — ColonyGraph not available")
1671
+
1672
+ # --- 6. Embedding pipeline ---
1673
+ embed_provider = os.environ.get("COLONY_EMBED_PROVIDER", "")
1674
+ embed_model = os.environ.get("COLONY_EMBED_MODEL", "")
1675
+ embed_dims = os.environ.get("COLONY_EMBED_DIMS", "")
1676
+ reranker_model = os.environ.get("COLONY_RERANKER_MODEL", "")
1677
+
1678
+ # Auto-detect tier if not explicitly configured
1679
+ if embed_provider != "skip" and (not embed_provider or not embed_model):
1680
+ try:
1681
+ from apsimo.vector.scanner import scan
1682
+ from apsimo.vector.tiers import get_tier_by_memory
1683
+ hw = scan()
1684
+ tier = get_tier_by_memory(hw.vram_gb, hw.ram_gb)
1685
+ spec = tier.text_embedder
1686
+ if spec:
1687
+ if hw.gpu_type == "cuda":
1688
+ embed_provider = embed_provider or "cuda"
1689
+ elif hw.gpu_type == "mlx":
1690
+ # Prefer native MLX when the package is available
1691
+ try:
1692
+ import mlx_embeddings # noqa: F401
1693
+ embed_provider = embed_provider or "native_mlx"
1694
+ except ImportError:
1695
+ embed_provider = embed_provider or "mlx"
1696
+ else:
1697
+ embed_provider = embed_provider or "cpu"
1698
+ embed_model = embed_model or spec.model_id
1699
+ embed_dims = embed_dims or str(spec.dims)
1700
+ reranker_model = reranker_model or (tier.text_reranker.model_id if tier.text_reranker else "")
1701
+ logger.info(
1702
+ "Auto-detected embedding tier: %s (GPU=%s %dGB, RAM=%dGB) -> %s",
1703
+ tier.label, hw.gpu_name, hw.vram_gb, hw.ram_gb, spec.model_id,
1704
+ )
1705
+ except Exception as exc:
1706
+ logger.warning("Hardware scan failed, using defaults: %s", exc)
1707
+ embed_provider = embed_provider or "cpu"
1708
+ embed_model = embed_model or "sentence-transformers/all-MiniLM-L6-v2"
1709
+ embed_dims = embed_dims or "384"
1710
+
1711
+ try:
1712
+ from apsimo.vector.embedder import EmbeddingPipeline
1713
+ from apsimo.vector.config import EmbeddingConfig
1714
+ embed_config = EmbeddingConfig(
1715
+ provider=embed_provider,
1716
+ model_id=embed_model,
1717
+ dimensions=int(embed_dims) if embed_dims else 384,
1718
+ revision=os.environ.get("COLONY_EMBED_REVISION") or None,
1719
+ )
1720
+ from apsimo.vector.embedder import make_provider
1721
+ provider = make_provider(embed_config)
1722
+ if provider is not None and embed_provider == "openai_api" and hasattr(provider, "configure"):
1723
+ # openai_api needs explicit endpoint config (the text-only path
1724
+ # never called configure(); only the multimodal branch passed these).
1725
+ provider.configure(
1726
+ os.environ.get("COLONY_EMBED_BASE_URL", ""),
1727
+ os.environ.get("COLONY_EMBED_API_KEY", ""),
1728
+ )
1729
+ if provider is None:
1730
+ # 'skip' provider — embeddings disabled entirely
1731
+ logger.info("EmbeddingPipeline skipped (provider=skip) — embeddings disabled")
1732
+ else:
1733
+ pipeline = EmbeddingPipeline(provider)
1734
+
1735
+ # Wire up multimodal if enabled
1736
+ multimodal_enabled = os.environ.get("COLONY_MULTIMODAL", "false").lower() == "true"
1737
+ if multimodal_enabled:
1738
+ try:
1739
+ from apsimo.vector.multimodal_provider import make_multimodal_provider
1740
+ from apsimo.vector.image_store import make_image_store
1741
+
1742
+ mm_config = EmbeddingConfig(
1743
+ provider=embed_provider,
1744
+ model_id=embed_model,
1745
+ dimensions=int(embed_dims) if embed_dims else 1024,
1746
+ base_url=os.environ.get("COLONY_EMBED_BASE_URL"),
1747
+ api_key=os.environ.get("COLONY_EMBED_API_KEY"),
1748
+ )
1749
+ mm_provider = make_multimodal_provider(mm_config)
1750
+ img_store = make_image_store(
1751
+ mode=os.environ.get("COLONY_IMAGE_STORAGE", "local"),
1752
+ state_dir=os.environ.get("COLONY_STATE_DIR", "."),
1753
+ )
1754
+ pipeline = EmbeddingPipeline(
1755
+ provider=provider,
1756
+ multimodal_provider=mm_provider,
1757
+ image_store=img_store,
1758
+ )
1759
+ logger.info("Multimodal enabled (model=%s, storage=%s)", embed_model, os.environ.get("COLONY_IMAGE_STORAGE", "local"))
1760
+ except Exception as exc:
1761
+ logger.warning("Multimodal init failed, falling back to text-only: %s", exc)
1762
+
1763
+ await pipeline.warmup()
1764
+ set_embedder(pipeline)
1765
+ logger.info("EmbeddingPipeline initialized (provider=%s model=%s)", embed_provider, embed_model)
1766
+
1767
+ # Wire embedding pipeline into ColonyGraph for vector-backed recall
1768
+ try:
1769
+ from apsimo.vector.store import VectorStore
1770
+ from apsimo.vector.indexes import IndexCatalog
1771
+ from apsimo.turns import get_turn_idempotency_ledger
1772
+ from apsimo.vector import set_store, set_pipeline
1773
+ vector_db_path = os.path.join(state_dir, "lancedb")
1774
+ vs = VectorStore(data_dir=vector_db_path, identity=pipeline.index_identity,
1775
+ catalog=IndexCatalog(get_turn_idempotency_ledger(state_dir)))
1776
+ embed_dims = int(os.environ.get("COLONY_EMBED_DIMS", pipeline.dimensions or 384))
1777
+ await vs.connect(dimensions=embed_dims)
1778
+ await vs.ensure_collections(dimensions=embed_dims)
1779
+ set_store(vs)
1780
+ set_pipeline(pipeline)
1781
+ if graph is not None:
1782
+ graph.set_embed_fn(pipeline.embed)
1783
+ graph.set_vector_store(vs)
1784
+ logger.info("ColonyGraph wired to vector store (path=%s)", vector_db_path)
1785
+
1786
+ if graph is not None and graph._embed_fn and graph._vector_store:
1787
+ logger.info("ColonyGraph fully operational (Neo4j + embeddings + vector store)")
1788
+ else:
1789
+ logger.warning("ColonyGraph partially wired — memory may be degraded")
1790
+ except Exception as vexc:
1791
+ logger.warning("Vector store wiring failed (recall will use keyword fallback): %s", vexc)
1792
+
1793
+ # Pass LLM config to pipeline for auto-captioning
1794
+ llm_config_path = Path(os.environ.get("COLONY_STATE_DIR", ".")) / ".colony-llm-config.json"
1795
+ if llm_config_path.exists() and hasattr(pipeline, "set_llm_config"):
1796
+ try:
1797
+ llm_cfg = _json.loads(llm_config_path.read_text())
1798
+ pipeline.set_llm_config(llm_cfg)
1799
+ logger.info("LLM config passed to EmbeddingPipeline for auto-captioning")
1800
+ except Exception as exc:
1801
+ logger.debug("Could not pass LLM config to pipeline: %s", exc)
1802
+
1803
+ # Health check + model mismatch detection
1804
+ try:
1805
+ hc = await pipeline.health_check()
1806
+ if hc.get("status") != "ok":
1807
+ logger.warning("Embedder health check failed: %s", hc.get("error", "unknown"))
1808
+ else:
1809
+ logger.info("Embedder health check passed (latency=%.1fms)", hc.get("latency_ms", 0))
1810
+ except Exception as exc:
1811
+ logger.warning("Embedder health check exception: %s", exc)
1812
+ except Exception as exc:
1813
+ logger.warning("EmbeddingPipeline init failed: %s", exc)
1814
+
1815
+ # --- 6b. Reranker pipeline ---
1816
+ reranker_provider_name = os.environ.get("COLONY_RERANKER_PROVIDER", "")
1817
+ if reranker_model and reranker_model.lower() not in ("none", "", "null"):
1818
+ try:
1819
+ from apsimo.vector.reranker import (
1820
+ OpenAIAPIRerankerProvider,
1821
+ NativeMLXRerankerProvider,
1822
+ MLXRerankerProvider,
1823
+ CPURerankerProvider,
1824
+ CUDARerankerProvider,
1825
+ )
1826
+ reranker_base_url = os.environ.get("COLONY_RERANKER_BASE_URL", "")
1827
+ reranker_api_key = os.environ.get("COLONY_RERANKER_API_KEY", "")
1828
+ if reranker_provider_name == "openai_api" or reranker_base_url:
1829
+ # Remote reranker over an OpenAI/Jina-compatible /v1/rerank
1830
+ # endpoint, mirroring the embedder's openai_api path so the
1831
+ # model stays off-box instead of loading in-process.
1832
+ # COLONY_RERANKER_PROMPT_STYLE=qwen3 applies the Qwen3-Reranker
1833
+ # instruction template, without which its scores are noise.
1834
+ reranker_provider = OpenAIAPIRerankerProvider(reranker_model)
1835
+ reranker_provider.configure(
1836
+ reranker_base_url,
1837
+ reranker_api_key,
1838
+ os.environ.get("COLONY_RERANKER_PROMPT_STYLE", ""),
1839
+ )
1840
+ else:
1841
+ from apsimo.vector.scanner import scan
1842
+ hw = scan()
1843
+ if hw.gpu_type == "mlx":
1844
+ # Prefer native MLX when the package is available
1845
+ try:
1846
+ import mlx_lm # noqa: F401
1847
+ reranker_provider = NativeMLXRerankerProvider(reranker_model)
1848
+ except ImportError:
1849
+ reranker_provider = MLXRerankerProvider(reranker_model)
1850
+ elif hw.gpu_type == "cuda":
1851
+ reranker_provider = CUDARerankerProvider(reranker_model)
1852
+ else:
1853
+ reranker_provider = CPURerankerProvider(reranker_model)
1854
+ await reranker_provider.warmup()
1855
+ set_reranker(reranker_provider)
1856
+ logger.info(
1857
+ "Reranker initialized (provider=%s model=%s)",
1858
+ reranker_provider_name or "local", reranker_model,
1859
+ )
1860
+ # Wire the reranker into ColonyGraph recall (mirrors the
1861
+ # set_embed_fn wiring above). Registration alone changes
1862
+ # nothing: use is gated by COLONY_RECALL_RERANK (default off).
1863
+ if graph is not None and hasattr(graph, "set_rerank_fn"):
1864
+ from apsimo.intelligence.graph.recall import provider_calibration_metadata
1865
+ def recall_calibration_metadata():
1866
+ return provider_calibration_metadata(reranker_provider)
1867
+ graph.set_rerank_fn(
1868
+ reranker_provider.rerank,
1869
+ calibration_metadata=recall_calibration_metadata)
1870
+ logger.info(
1871
+ "ColonyGraph wired to reranker for recall "
1872
+ "(gated by COLONY_RECALL_RERANK)")
1873
+ except Exception as exc:
1874
+ logger.warning("Reranker init failed: %s", exc)
1875
+ else:
1876
+ logger.info("No reranker configured for this tier")
1877
+
1878
+ # --- 7. Goals engine ---
1879
+ goals_engine = None
1880
+ try:
1881
+ from apsimo.goals.engine import GoalEngine
1882
+ from apsimo.goals.store import GoalStore
1883
+ goals_db = os.path.join(state_dir, "colony-goals.db")
1884
+ goals_store = GoalStore(db_path=goals_db)
1885
+ goals_engine = GoalEngine(store=goals_store)
1886
+ set_goals_engine(goals_engine)
1887
+ logger.info("GoalEngine initialized (db=%s)", goals_db)
1888
+ except Exception as exc:
1889
+ logger.warning("GoalEngine init failed: %s", exc)
1890
+
1891
+ # --- 7b. Commitment Store ---
1892
+ try:
1893
+ from apsimo.commitments.store import CommitmentStore
1894
+
1895
+ commitments_db = state_dir / "colony-commitments.db"
1896
+ commitment_store = CommitmentStore(db_path=commitments_db)
1897
+ commitment_readiness = (
1898
+ commitment_store.resolution_recovery_readiness()
1899
+ )
1900
+ if commitment_readiness.get("ready") is not True:
1901
+ raise RuntimeError(
1902
+ "CommitmentStore resolution recovery is unavailable"
1903
+ )
1904
+
1905
+ # Resolving a workspace concern raised from a commitment settles the
1906
+ # commitment itself — without this, the ingest loop re-raises the
1907
+ # concern from the still-open commitment and the resolve is cosmetic.
1908
+ from apsimo.self_model.settlement import register_settler
1909
+
1910
+ def _settle_commitment(source_id, *, outcome="done", note="",
1911
+ resolved_by="owner", operation_id=None,
1912
+ _cs=commitment_store):
1913
+ row = _cs.resolve(source_id, outcome=outcome, note=note,
1914
+ resolved_by=resolved_by,
1915
+ operation_id=operation_id)
1916
+ if not row:
1917
+ return None
1918
+ operation = (
1919
+ _cs.get_resolution_operation(source_id)
1920
+ if operation_id is not None else None
1921
+ )
1922
+ resolution = operation or (
1923
+ (row.get("metadata") or {}).get("resolution") or {}
1924
+ )
1925
+ return {
1926
+ "kind": "commitment",
1927
+ "status": row["status"],
1928
+ "operation_id": resolution.get("operation_id"),
1929
+ "outcome": resolution.get("outcome"),
1930
+ "note_digest": resolution.get("note_digest"),
1931
+ "resolved_by": (
1932
+ resolution.get("resolved_by")
1933
+ if operation is not None else resolution.get("by")
1934
+ ),
1935
+ }
1936
+
1937
+ register_settler("commitment", _settle_commitment, retry_safe=True)
1938
+ set_commitment_store(commitment_store)
1939
+ logger.info(
1940
+ "CommitmentStore initialized (db=%s, capability=%s)",
1941
+ commitments_db, commitment_readiness["capability"],
1942
+ )
1943
+ except Exception as exc:
1944
+ set_commitment_store(None)
1945
+ logger.error("CommitmentStore initialization failed")
1946
+ raise RuntimeError("CommitmentStore initialization failed") from exc
1947
+
1948
+ # --- 7c. Theory of Mind ---
1949
+ try:
1950
+ from apsimo.tom.affect import AffectStore
1951
+ from apsimo.tom.facts import SharedFactsStore
1952
+
1953
+ from apsimo.turns import get_turn_idempotency_ledger
1954
+ source_ledger = get_turn_idempotency_ledger(state_dir)
1955
+ affect_db = state_dir / "colony-affect.db"
1956
+ affect_store = AffectStore(db_path=affect_db, source_ledger=source_ledger)
1957
+ affect_store.purge_erased_sources()
1958
+ set_affect_store(affect_store)
1959
+ logger.info("AffectStore initialized (db=%s)", affect_db)
1960
+
1961
+ facts_db = state_dir / "colony-facts.db"
1962
+ facts_store = SharedFactsStore(db_path=facts_db, source_ledger=source_ledger)
1963
+ facts_store.purge_erased_sources()
1964
+ set_facts_store(facts_store)
1965
+ logger.info("SharedFactsStore initialized (db=%s)", facts_db)
1966
+
1967
+ try:
1968
+ _p8_wiring = _attach_p8_runtime(
1969
+ state_dir=state_dir, facts_store=facts_store, graph=graph)
1970
+ if _p8_wiring is not None:
1971
+ logger.info(
1972
+ "P8 visibility/arcs/recipient audit attached (shadow only)")
1973
+ except Exception:
1974
+ # P8 is an advisory shadow integration. Its persistence must not
1975
+ # take down the canonical SharedFactsStore or the rest of ToM.
1976
+ _p8_wiring = None
1977
+ logger.warning(
1978
+ "P8 shadow attachment failed; continuing with P8 off",
1979
+ exc_info=True,
1980
+ )
1981
+
1982
+ # Second-order theory of mind (tom2): refs-not-content inference
1983
+ # store + daily asymmetry engine. Inert unless COLONY_TOM2 is set
1984
+ # (default off; shadow = counts only).
1985
+ from apsimo.tom.tom2 import Tom2Store
1986
+ from apsimo.tom.asymmetry import AsymmetryEngine, tom2_mode
1987
+ from apsimo.api.routers.host import set_tom2_store, set_tom2_engine
1988
+ tom2_db = state_dir / "colony-tom2.db"
1989
+ tom2_store = Tom2Store(db_path=str(tom2_db))
1990
+ set_tom2_store(tom2_store)
1991
+ set_tom2_engine(AsymmetryEngine(facts_store, tom2_store))
1992
+ logger.info("Tom2Store + AsymmetryEngine initialized (db=%s, mode=%s)",
1993
+ tom2_db, tom2_mode())
1994
+
1995
+ # Level-2 exposure ledger (L2.3): refs-only record of any future
1996
+ # level-2 rendering + its budgets. Empty and inert until the leveled
1997
+ # rendering path is wired; the owner endpoint reads it regardless.
1998
+ from apsimo.tom.exposure import Tom2ExposureStore
1999
+ from apsimo.api.routers.host import set_tom2_exposure_store
2000
+ tom2_exposure_db = state_dir / "colony-tom2-exposure.db"
2001
+ set_tom2_exposure_store(Tom2ExposureStore(db_path=str(tom2_exposure_db)))
2002
+ logger.info("Tom2ExposureStore initialized (db=%s)", tom2_exposure_db)
2003
+
2004
+ # Conversation presence registry (L1.1): passive census of WHO was
2005
+ # seen in WHICH conversation, fed from the turns/sync attribution
2006
+ # chokepoint. Read by the environment-risk classifier; recording is
2007
+ # gated by COLONY_CONV_PRESENCE (default on).
2008
+ from apsimo.channels.presence import ConversationPresenceStore
2009
+ from apsimo.api.routers.host import set_presence_store
2010
+ presence_db = state_dir / "colony-presence.db"
2011
+ presence_store = ConversationPresenceStore(db_path=str(presence_db))
2012
+ set_presence_store(presence_store)
2013
+ logger.info("ConversationPresenceStore initialized (db=%s)", presence_db)
2014
+
2015
+ from apsimo.gate.context_provenance import (
2016
+ ContextProvenanceStore, ProvenanceCrossContextGuard)
2017
+ provenance_db = state_dir / "colony-context-provenance.db"
2018
+ provenance_store = ContextProvenanceStore(db_path=str(provenance_db))
2019
+ set_context_provenance_store(provenance_store)
2020
+ logger.info("ContextProvenanceStore initialized (db=%s)", provenance_db)
2021
+
2022
+ # Outbound response gate (opt-in; shadow by default). Applicability is
2023
+ # resolved by the static, deployment-neutral surface policy: guarded
2024
+ # text/artifacts and excluded real-time speech. Gateway labels grant
2025
+ # no bypass authority.
2026
+ from apsimo.gate.response_guard import ResponseGuard, GuardMode
2027
+ from apsimo.gate.surface_policy import (
2028
+ POLICY_DIGEST as _guard_policy_digest,
2029
+ POLICY_ID as _guard_policy_id,
2030
+ )
2031
+ from apsimo.world_model.extraction.conversation_extractor import (
2032
+ ConversationExtractor)
2033
+ _guard_mode = _resolve_guard_mode(os.environ.get("COLONY_GUARD_MODE"))
2034
+ from apsimo.gate.guard_audit import GuardAuditStore
2035
+ guard_audit_db = state_dir / "colony-guard-audit.db"
2036
+ guard_audit_store = GuardAuditStore(db_path=str(guard_audit_db))
2037
+ # Injection-taint registry (L3.1) + tom2 epistemic egress net (L3.2).
2038
+ # The check is INERT (zero findings) until the leveled tom2 wiring
2039
+ # registers a taint, which is why it may sit on the default enforce
2040
+ # allowlist from day one.
2041
+ from apsimo.gate.taint import TaintRegistry
2042
+ from apsimo.gate.layers.tom2_epistemic import Tom2EpistemicGuard
2043
+ from apsimo.api.routers.host import set_taint_registry
2044
+ taint_db = state_dir / "colony-tom2-taint.db"
2045
+ taint_registry = TaintRegistry(db_path=str(taint_db))
2046
+ set_taint_registry(taint_registry)
2047
+ _guard = ResponseGuard(
2048
+ cross_context=ProvenanceCrossContextGuard(provenance_store, extractor=ConversationExtractor()),
2049
+ default_mode=_guard_mode, audit_store=guard_audit_store,
2050
+ tom2_epistemic=Tom2EpistemicGuard(taint_registry, facts_store=facts_store))
2051
+ set_response_guard(_guard)
2052
+ logger.info(
2053
+ "ResponseGuard initialized (mode=%s, surface_policy=%s:%s, "
2054
+ "audit=%s, taints=%s)",
2055
+ _guard_mode.value, _guard_policy_id, _guard_policy_digest,
2056
+ guard_audit_db, taint_db,
2057
+ )
2058
+ # Verdict rows prove evaluation, not that an egress mediator actually
2059
+ # withheld or revised the exact bytes. Keep Tom2 capped at level 1
2060
+ # until a digest-bound applied-output receipt exists.
2061
+ from apsimo.tom.levels import set_evidence_probe
2062
+ set_evidence_probe(None)
2063
+
2064
+ from apsimo.tom.engagement import EngagementStore
2065
+ engagement_db = state_dir / "colony-engagement.db"
2066
+ engagement_store = EngagementStore(db_path=engagement_db, source_ledger=source_ledger)
2067
+ engagement_store.purge_erased_sources()
2068
+ set_engagement_store(engagement_store)
2069
+ logger.info("EngagementStore initialized (db=%s)", engagement_db)
2070
+
2071
+ from apsimo.contacts.comms import CommsLog
2072
+ comms_log = CommsLog(db_path=state_dir / "colony-comms.db")
2073
+ set_comms_log(comms_log)
2074
+ logger.info("CommsLog initialized")
2075
+
2076
+ # Owner preference learner — captures the owner's *explicit* directives
2077
+ # about how to communicate ("be concise", "use bullets", "no emoji") at
2078
+ # high confidence; complements the inferred per-contact EngagementStore.
2079
+ from apsimo.intelligence.components.preference_learner import PreferenceLearner
2080
+ from apsimo.self_model.perspective import SelfPerspective
2081
+ from apsimo.turns import get_turn_idempotency_ledger
2082
+ from apsimo.identity import get_owner_contact_id
2083
+ preference_learner = PreferenceLearner(db_path=str(state_dir / "colony-preferences.db"),
2084
+ perspective=SelfPerspective(get_turn_idempotency_ledger(state_dir), owner_id=get_owner_contact_id()))
2085
+ set_preference_learner(preference_learner)
2086
+ logger.info("PreferenceLearner initialized (db=%s)", state_dir / "colony-preferences.db")
2087
+ except Exception as exc:
2088
+ try:
2089
+ from apsimo.tom.levels import set_evidence_probe
2090
+ set_evidence_probe(None)
2091
+ except Exception:
2092
+ logger.debug("Tom2 evidence probe init cleanup failed", exc_info=True)
2093
+ logger.warning("Theory of Mind init failed: %s", exc)
2094
+
2095
+ # --- Directive / boundary memory (safety foundation) ---
2096
+ # Durable store of the owner's standing directives (MUST NOT / MUST) with an
2097
+ # enforcement guard consulted before autonomous actions. Boundaries must be
2098
+ # available before any action-taking, so this is wired unconditionally.
2099
+ try:
2100
+ from apsimo.directives import DirectiveManager, DirectiveStore
2101
+ from apsimo.api.routers.host import set_directive_manager
2102
+ from apsimo.turns import get_turn_idempotency_ledger
2103
+ _directive_store = DirectiveStore(db_path=str(state_dir / "colony-directives.db"),
2104
+ ledger=get_turn_idempotency_ledger(state_dir))
2105
+ _directive_manager = DirectiveManager(_directive_store)
2106
+ set_directive_manager(_directive_manager)
2107
+ if locals().get("tool_executor") is not None:
2108
+ tool_executor.configure_execution_policy(
2109
+ directive_manager=_directive_manager,
2110
+ boundary_required=True,
2111
+ )
2112
+ logger.info(
2113
+ "DirectiveManager initialized (db=%s, active=%d)",
2114
+ state_dir / "colony-directives.db", _directive_store.count_active(),
2115
+ )
2116
+ except Exception as exc:
2117
+ if locals().get("tool_executor") is not None:
2118
+ # A configured server may keep public information tools usable,
2119
+ # but private reads/mutations cannot silently lose their boundary.
2120
+ tool_executor.configure_execution_policy(
2121
+ directive_manager=None,
2122
+ boundary_required=True,
2123
+ )
2124
+ logger.warning("DirectiveManager init failed (boundaries disabled): %s", exc)
2125
+
2126
+ # --- Proposal store (self-directed thinking + research -> proposals) ---
2127
+ try:
2128
+ from apsimo.proposals import ProposalStore
2129
+ from apsimo.api.routers.host import set_proposal_store
2130
+ _proposal_store = ProposalStore(db_path=str(state_dir / "colony-proposals.db"))
2131
+ set_proposal_store(_proposal_store)
2132
+ logger.info("ProposalStore initialized (db=%s)", state_dir / "colony-proposals.db")
2133
+ except Exception as exc:
2134
+ logger.warning("ProposalStore init failed: %s", exc)
2135
+
2136
+ # --- Type feedback store (outcome-driven priority decay/boost) ---
2137
+ try:
2138
+ from apsimo.feedback import TypeFeedbackStore
2139
+ from apsimo.api.routers.host import set_feedback_store
2140
+ set_feedback_store(TypeFeedbackStore(db_path=str(state_dir / "colony-feedback.db")))
2141
+ logger.info("TypeFeedbackStore initialized (db=%s)", state_dir / "colony-feedback.db")
2142
+ except Exception as exc:
2143
+ logger.warning("TypeFeedbackStore init failed: %s", exc)
2144
+
2145
+ # --- Self-model / trust engine + action journal (item 4, Amendment 1) ---
2146
+ # Wired before directed action so approval tiering can consult trust.
2147
+ _sm_for_directed = None
2148
+ try:
2149
+ from apsimo.self_model import (
2150
+ ActionJournal, CompetenceStore, SelfModel, TrustEngine,
2151
+ self_model_enabled,
2152
+ )
2153
+ from apsimo.api.routers.host import (
2154
+ set_self_model, _feedback_store as _fb_for_trust,
2155
+ )
2156
+ if self_model_enabled():
2157
+ from apsimo.autonomy.registry import SubsystemRegistry as _Reg
2158
+ _competence = CompetenceStore(
2159
+ db_path=str(state_dir / "colony-self-model.db"))
2160
+ _journal = ActionJournal(
2161
+ db_path=str(state_dir / "colony-action-journal.db"))
2162
+ _trust = TrustEngine(
2163
+ _competence, db_path=str(state_dir / "colony-self-model.db"),
2164
+ feedback_store=_fb_for_trust, journal=_journal)
2165
+ _sm_for_directed = SelfModel(_competence, registry=_Reg(),
2166
+ trust=_trust, journal=_journal)
2167
+ _sm_for_directed.perspective = getattr(locals().get('preference_learner'), 'perspective', None)
2168
+ set_self_model(_sm_for_directed)
2169
+ if _adaptive_params is not None:
2170
+ _adaptive_params.set_journal(_journal)
2171
+ logger.info(
2172
+ "SelfModel/TrustEngine initialized (db=%s, journal=%s, "
2173
+ "autograduate=%s)",
2174
+ state_dir / "colony-self-model.db",
2175
+ state_dir / "colony-action-journal.db",
2176
+ os.environ.get("COLONY_TRUST_AUTOGRADUATE", "true"))
2177
+ else:
2178
+ logger.info("SelfModel disabled (COLONY_SELF_MODEL_ENABLED=false)")
2179
+ except Exception as exc:
2180
+ logger.warning("SelfModel init failed: %s", exc)
2181
+
2182
+ # --- P4 controlled learning: one evidence and authority graph ---
2183
+ _controlled_learning = {
2184
+ "corrections": None,
2185
+ "benchmark": None,
2186
+ "experiments": None,
2187
+ "approval_authority": None,
2188
+ }
2189
+ try:
2190
+ _controlled_learning = _initialize_controlled_learning(
2191
+ state_dir=state_dir,
2192
+ adaptive_params=_adaptive_params,
2193
+ journal=locals().get("_journal"),
2194
+ )
2195
+ if _controlled_learning["benchmark"] is not None:
2196
+ logger.info(
2197
+ "Selfhood benchmark ready (db=%s, corrections=%s)",
2198
+ state_dir / "colony-benchmark.db",
2199
+ state_dir / "colony-learning-feedback.db",
2200
+ )
2201
+ else:
2202
+ logger.info(
2203
+ "Selfhood benchmark disabled (COLONY_BENCHMARK_ENABLED=false)")
2204
+ if _controlled_learning["experiments"] is not None:
2205
+ logger.info(
2206
+ "Controlled experiment framework ready (db=%s, shared_authority=%s)",
2207
+ state_dir / "colony-experiments.db",
2208
+ state_dir / "approval_authority.db",
2209
+ )
2210
+ else:
2211
+ logger.info(
2212
+ "Experiment framework disabled "
2213
+ "(COLONY_EXPERIMENTS_ENABLED=false)")
2214
+ except Exception as exc:
2215
+ logger.error("Controlled learning init failed closed: %s", exc)
2216
+
2217
+ # --- Toolsmith (Mind M1): self-built, sandbox-verified tools ---
2218
+ try:
2219
+ from apsimo.toolsmith import (
2220
+ Toolsmith, ToolRegistry, toolsmith_enabled,
2221
+ )
2222
+ from apsimo.api.routers.host import set_toolsmith
2223
+ if toolsmith_enabled():
2224
+ _tool_registry = ToolRegistry(
2225
+ db_path=str(state_dir / "colony-toolsmith.db"),
2226
+ library_root=str(state_dir / "toolsmith_library"))
2227
+ _toolsmith = Toolsmith(_tool_registry)
2228
+ set_toolsmith(_toolsmith)
2229
+ # advertise graduated tools to the reasoning loop
2230
+ try:
2231
+ from apsimo.api.routers.host import _tool_executor
2232
+ if _tool_executor is not None:
2233
+ _tool_executor.set_dynamic_provider(
2234
+ _toolsmith.build_dynamic_provider())
2235
+ except Exception as texc:
2236
+ logger.warning("toolsmith dynamic provider wiring: %s", texc)
2237
+ logger.info("Toolsmith ready (mode=%s, db=%s)",
2238
+ os.environ.get("COLONY_TOOLSMITH", "off"),
2239
+ state_dir / "colony-toolsmith.db")
2240
+ else:
2241
+ logger.info("Toolsmith disabled (COLONY_TOOLSMITH=off)")
2242
+ except Exception as exc:
2243
+ logger.warning("Toolsmith init failed: %s", exc)
2244
+
2245
+ # --- Expectation engine (Mind M3a): predictions + surprise + calibration ---
2246
+ try:
2247
+ from apsimo.self_model.expectations import (
2248
+ ExpectationEngine, ExpectationStore, expectations_enabled,
2249
+ expectations_mode,
2250
+ )
2251
+ from apsimo.api.routers.host import set_expectations
2252
+ if expectations_enabled():
2253
+ _exp_store = ExpectationStore(
2254
+ db_path=str(state_dir / "colony-expectations.db"))
2255
+ _exp_journal = None
2256
+ try:
2257
+ from apsimo.api.routers.host import _self_model as _sm_e
2258
+ _exp_journal = getattr(_sm_e, "journal", None)
2259
+ except Exception:
2260
+ _exp_journal = None
2261
+ # workspace wired later; set_expectations stores the engine and the
2262
+ # autonomy phase links the workspace ref at runtime.
2263
+ _expectations = ExpectationEngine(_exp_store, journal=_exp_journal)
2264
+ set_expectations(_expectations)
2265
+ # World-model prediction classes (relationship-still-active,
2266
+ # property-unchanged) — registered here, guarded on the engine;
2267
+ # the resolvers fetch the world store lazily so boot order and
2268
+ # a missing world model are both safe (they resolve to None).
2269
+ try:
2270
+ from apsimo.world_model.expectation_resolvers import (
2271
+ register_world_resolvers,
2272
+ )
2273
+ register_world_resolvers(_expectations)
2274
+ except Exception as rexc:
2275
+ logger.warning("World expectation resolvers not registered: "
2276
+ "%s", rexc)
2277
+ logger.info("Expectation engine ready (mode=%s, db=%s)",
2278
+ expectations_mode(),
2279
+ state_dir / "colony-expectations.db")
2280
+ else:
2281
+ logger.info("Expectation engine disabled (COLONY_EXPECTATIONS=off)")
2282
+ except Exception as exc:
2283
+ logger.warning("Expectation engine init failed: %s", exc)
2284
+
2285
+ # --- Cognitive workspace (Mind M2): continuity of thought ---
2286
+ try:
2287
+ from apsimo.self_model.workspace import (
2288
+ ConcernStore, WorkspaceEngine, workspace_enabled, workspace_mode,
2289
+ )
2290
+ from apsimo.self_model.thinker import build_thinker
2291
+ from apsimo.self_model.event_concerns import (
2292
+ ConversationTurnConcernReducer,
2293
+ EventConcernReducer,
2294
+ ExternalEventConcernReducer,
2295
+ event_concerns_enabled,
2296
+ event_concern_mode,
2297
+ external_event_concerns_enabled,
2298
+ external_event_concern_mode,
2299
+ turn_concerns_enabled,
2300
+ turn_concern_channels,
2301
+ turn_concern_mode,
2302
+ )
2303
+ from apsimo.api.routers.host import set_workspace
2304
+ if workspace_enabled():
2305
+ _concern_store = ConcernStore(
2306
+ db_path=str(state_dir / "colony-workspace.db"))
2307
+ _thinker = (build_thinker(llm_router, graph=graph)
2308
+ if llm_router is not None else None)
2309
+ _ws_journal = None
2310
+ try:
2311
+ from apsimo.api.routers.host import _self_model as _sm_ws
2312
+ _ws_journal = getattr(_sm_ws, "journal", None)
2313
+ except Exception:
2314
+ _ws_journal = None
2315
+ _workspace = WorkspaceEngine(
2316
+ _concern_store, thinker=_thinker, journal=_ws_journal)
2317
+ if event_concerns_enabled():
2318
+ _workspace.event_reducer = EventConcernReducer(_concern_store)
2319
+ logger.info(
2320
+ "Durable event-to-concern reducer ready (mode=%s, bootstrap=%s)",
2321
+ event_concern_mode(),
2322
+ os.environ.get("COLONY_EVENT_CONCERNS_BOOTSTRAP", "tail"),
2323
+ )
2324
+ if external_event_concerns_enabled():
2325
+ _workspace.external_event_reducer = ExternalEventConcernReducer(
2326
+ _concern_store,
2327
+ )
2328
+ logger.info(
2329
+ "External event-to-concern reducer ready "
2330
+ "(mode=%s, bootstrap=replay)",
2331
+ external_event_concern_mode(),
2332
+ )
2333
+ if turn_concerns_enabled():
2334
+ _workspace.turn_event_reducer = ConversationTurnConcernReducer(
2335
+ _concern_store,
2336
+ )
2337
+ logger.info(
2338
+ "Conversation turn-to-concern reducer ready "
2339
+ "(mode=%s, bootstrap=%s, channels=%s)",
2340
+ turn_concern_mode(),
2341
+ os.environ.get("COLONY_TURN_CONCERNS_BOOTSTRAP", "tail"),
2342
+ ",".join(turn_concern_channels()) or "<none>",
2343
+ )
2344
+ set_workspace(_workspace)
2345
+ logger.info("Cognitive workspace ready (mode=%s, db=%s)",
2346
+ workspace_mode(), state_dir / "colony-workspace.db")
2347
+ else:
2348
+ logger.info("Cognitive workspace disabled (COLONY_WORKSPACE=off)")
2349
+ if external_event_concerns_enabled():
2350
+ logger.warning(
2351
+ "External event concerns requested but workspace is disabled"
2352
+ )
2353
+ if turn_concerns_enabled():
2354
+ logger.warning(
2355
+ "Conversation turn concerns requested but workspace is disabled"
2356
+ )
2357
+ except Exception as exc:
2358
+ logger.warning("Workspace init failed: %s", exc)
2359
+
2360
+ # --- Skills memory (procedure memory, item 3) ---
2361
+ _skills_mem_store = None
2362
+ try:
2363
+ from apsimo.skills_memory import SkillStore, skills_distill_mode
2364
+ from apsimo.api.routers.host import set_skill_store
2365
+ _skills_mem_store = SkillStore(
2366
+ db_path=str(state_dir / "colony-skills.db"))
2367
+ set_skill_store(_skills_mem_store)
2368
+ logger.info("SkillStore initialized (db=%s, %d skill(s), distill=%s)",
2369
+ state_dir / "colony-skills.db",
2370
+ _skills_mem_store.count(), skills_distill_mode())
2371
+ except Exception as exc:
2372
+ logger.warning("SkillStore init failed: %s", exc)
2373
+
2374
+ # --- Mining: escalation miner + verbatim turn capture (corpus source) ---
2375
+ try:
2376
+ from apsimo.mining import EscalationMiner, MiningStore, mining_mode
2377
+ from apsimo.api.routers.mining import set_mining
2378
+
2379
+ if mining_mode() != "off":
2380
+ _mining_store_obj = MiningStore(
2381
+ db_path=str(state_dir / "colony-mining.db"))
2382
+
2383
+ def _mining_router_getter():
2384
+ try:
2385
+ from apsimo.api.routers.host import _reasoning_loop
2386
+ return getattr(_reasoning_loop, "_model", None)
2387
+ except Exception:
2388
+ return None
2389
+
2390
+ _mining_engine_obj = EscalationMiner(
2391
+ _mining_store_obj,
2392
+ skill_store=_skills_mem_store,
2393
+ router_getter=_mining_router_getter,
2394
+ )
2395
+ set_mining(_mining_store_obj, _mining_engine_obj, state_dir)
2396
+ logger.info("EscalationMiner initialized (db=%s, mode=%s)",
2397
+ state_dir / "colony-mining.db", mining_mode())
2398
+ else:
2399
+ logger.info("Mining disabled (COLONY_ESCALATION_MINING=off)")
2400
+ except Exception as exc:
2401
+ logger.warning("Mining init failed: %s", exc)
2402
+
2403
+ # --- Read-only repo mirrors + directed action (option A) ---
2404
+ try:
2405
+ from apsimo.repos import RepoMirrorManager
2406
+ from apsimo.directed import (
2407
+ DirectedActionService, ScopedTaskStore, directed_mode,
2408
+ )
2409
+ from apsimo.api.routers.host import (
2410
+ set_repo_mirrors, set_directed_service,
2411
+ get_directive_manager as _get_dm2,
2412
+ )
2413
+ _mirrors_mgr = RepoMirrorManager(
2414
+ mirror_dir=str(state_dir / "repo-mirrors"),
2415
+ directive_manager=_get_dm2(),
2416
+ )
2417
+ set_repo_mirrors(_mirrors_mgr)
2418
+ _n_repos = len(_mirrors_mgr.configured())
2419
+ if _n_repos:
2420
+ # Clone/pull in the background so boot is not blocked by network.
2421
+ async def _sync_mirrors():
2422
+ try:
2423
+ loop_ = asyncio.get_event_loop()
2424
+ results = await loop_.run_in_executor(None, _mirrors_mgr.refresh_all)
2425
+ logger.info(
2426
+ "Repo mirrors synced: %s",
2427
+ {k: v.get("action") or v.get("reason") for k, v in results.items()},
2428
+ )
2429
+ from apsimo.api.routers.host import _world_store as _ws
2430
+ n = await _mirrors_mgr.register_entities(_ws)
2431
+ if n:
2432
+ logger.info("Registered %d repo(s) as Project entities", n)
2433
+ except Exception:
2434
+ logger.debug("mirror sync failed", exc_info=True)
2435
+ asyncio.create_task(_sync_mirrors())
2436
+ logger.info("RepoMirrorManager initialized (%d repo(s) configured)", _n_repos)
2437
+
2438
+ async def _directed_deliver(payload: dict) -> bool:
2439
+ # Late-bound: route through the autonomy loop's guarded delivery
2440
+ # (boundary + sanitize + rate + shadow), same as every reach-out.
2441
+ try:
2442
+ from apsimo.api.routers.host import _autonomy_loop, _delivery_bridge
2443
+ if _autonomy_loop is not None and _delivery_bridge is not None:
2444
+ return await _autonomy_loop._route_reachout_delivery(
2445
+ payload, _delivery_bridge)
2446
+ except Exception:
2447
+ logger.debug("directed deliver failed", exc_info=True)
2448
+ return False
2449
+
2450
+ from apsimo.api.routers.host import _feedback_store as _fb_store
2451
+ _directed_svc = DirectedActionService(
2452
+ store=ScopedTaskStore(db_path=str(state_dir / "colony-directed.db")),
2453
+ directive_manager=_get_dm2(),
2454
+ mirrors=_mirrors_mgr,
2455
+ feedback_store=_fb_store,
2456
+ delivery_router=_directed_deliver,
2457
+ self_model=_sm_for_directed,
2458
+ )
2459
+ set_directed_service(_directed_svc)
2460
+ logger.info("DirectedActionService initialized (mode=%s)", directed_mode())
2461
+
2462
+ # The once-per-boundary critical flag rides the same guarded delivery.
2463
+ _dm_for_flags = _get_dm2()
2464
+ if _dm_for_flags is not None:
2465
+ _dm_for_flags.set_delivery_router(_directed_deliver)
2466
+ except Exception as exc:
2467
+ logger.warning("Directed-action init failed: %s", exc)
2468
+
2469
+ # --- Pattern Extraction + Surprise ---
2470
+ try:
2471
+ from apsimo.patterns.store import PatternStore
2472
+ from apsimo.surprise.store import SurpriseStore
2473
+
2474
+ patterns_db = state_dir / "colony-patterns.db"
2475
+ pattern_store = PatternStore(db_path=patterns_db)
2476
+ set_pattern_store(pattern_store)
2477
+ logger.info("PatternStore initialized (db=%s)", patterns_db)
2478
+
2479
+ surprise_db = state_dir / "colony-surprise.db"
2480
+ surprise_store = SurpriseStore(db_path=surprise_db)
2481
+ set_surprise_store(surprise_store)
2482
+ logger.info("SurpriseStore initialized (db=%s)", surprise_db)
2483
+
2484
+ # Close the surprise loop: the condition worker's
2485
+ # surprise.accumulation event now lands as a workspace concern
2486
+ # (the consumer checks workspace enablement at event time, so this
2487
+ # registration is unconditional and a clean no-op when it is off).
2488
+ from apsimo.surprise.accumulation import (
2489
+ register as register_surprise_consumer,
2490
+ )
2491
+ register_surprise_consumer()
2492
+ except Exception as exc:
2493
+ logger.warning("Pattern/Surprise init failed: %s", exc)
2494
+
2495
+ # --- ToM LLM Extractor ---
2496
+ try:
2497
+ if llm_router is not None:
2498
+ from apsimo.tom.extractor import TomExtractor
2499
+ tom_extractor = TomExtractor(llm_router)
2500
+ set_tom_extractor(tom_extractor)
2501
+ logger.info("ToM LLM Extractor initialized (router=%s)", type(llm_router).__name__)
2502
+ else:
2503
+ logger.info("ToM LLM Extractor skipped — no LLM router")
2504
+ except Exception as exc:
2505
+ logger.warning("ToM Extractor init failed: %s", exc)
2506
+
2507
+ # --- 7e. Channel Registration Store ---
2508
+ channel_store = None
2509
+ try:
2510
+ from apsimo.channels.store import ChannelStore
2511
+ from apsimo.channels.router import set_channel_store
2512
+ from apsimo.channels.phone_gateways import set_channel_store_ref
2513
+
2514
+ channels_db = os.path.join(state_dir, "colony-channels.db")
2515
+ channel_store = ChannelStore(db_path=channels_db)
2516
+ channel_store.connect()
2517
+ set_channel_store(channel_store)
2518
+ set_channel_store_ref(channel_store)
2519
+ from apsimo.api.routers.host import set_channel_store as _host_set_channel_store
2520
+ _host_set_channel_store(channel_store) # turn traffic auto-registers + touches channels
2521
+ logger.info("ChannelStore initialized (db=%s)", channels_db)
2522
+ except Exception as exc:
2523
+ logger.warning("ChannelStore init failed: %s", exc)
2524
+
2525
+ # --- 8. Contacts ---
2526
+ contacts_store = None
2527
+ try:
2528
+ from apsimo.contacts.config import ContactsConfig
2529
+ from apsimo.contacts.store import SQLiteContactStore
2530
+ contacts_config = ContactsConfig.from_env()
2531
+ contacts_store = SQLiteContactStore(config=contacts_config, graph=graph)
2532
+ await contacts_store.connect()
2533
+ set_contacts_store(contacts_store)
2534
+ logger.info("ContactsStore initialized (path=%s)", contacts_config.sqlite_path)
2535
+ except Exception as exc:
2536
+ logger.warning("ContactsStore init failed: %s", exc)
2537
+
2538
+ # --- 8b. Contact-World Model Bridge ---
2539
+ if contacts_store is not None and graph is not None:
2540
+ try:
2541
+ from apsimo.contacts.world_bridge import WorldModelContactBridge
2542
+ bridge = WorldModelContactBridge(graph=graph, store=contacts_store)
2543
+ # Backfill all substantive Person nodes on startup
2544
+ backfill_stats = await bridge.backfill_all_people()
2545
+ logger.info(
2546
+ "WorldModelContactBridge initialized — backfill created=%d linked=%d skipped=%d",
2547
+ backfill_stats["created"], backfill_stats["linked"], backfill_stats["skipped"],
2548
+ )
2549
+ # Prune shadow contacts whose Person node no longer exists
2550
+ pruned = await bridge.prune_orphaned_shadows()
2551
+ if pruned:
2552
+ logger.info("Pruned %d orphaned shadow contacts", pruned)
2553
+ except Exception as exc:
2554
+ logger.warning("WorldModelContactBridge init failed: %s", exc)
2555
+ else:
2556
+ logger.info("WorldModelContactBridge skipped — contacts_store or graph unavailable")
2557
+
2558
+ # --- 8d. Relationship profiler (standing + psyche + approach briefs) ---
2559
+ if contacts_store is not None:
2560
+ try:
2561
+ from apsimo.intelligence.relationships.profiler import (
2562
+ RelationshipProfiler,
2563
+ )
2564
+ import apsimo.api.routers.host as _host_mod
2565
+ _rel_profiler = RelationshipProfiler(
2566
+ contacts_store=contacts_store,
2567
+ comms_log=_host_mod._comms_log,
2568
+ affect_store=_host_mod._affect_store,
2569
+ facts_store=_host_mod._facts_store,
2570
+ engagement_store=_host_mod._engagement_store,
2571
+ p8_runtime=_p8_wiring,
2572
+ db_path=str(state_dir / "colony-relationships.db"),
2573
+ )
2574
+ _host_mod.set_relationship_profiler(_rel_profiler)
2575
+ logger.info("RelationshipProfiler initialized (db=%s)",
2576
+ state_dir / "colony-relationships.db")
2577
+ except Exception as exc:
2578
+ logger.warning("RelationshipProfiler init failed: %s", exc)
2579
+
2580
+ # --- 9. Briefings ---
2581
+ try:
2582
+ from apsimo.briefings.engine import BriefingEngine
2583
+ briefings = BriefingEngine()
2584
+ set_briefings_engine(briefings)
2585
+ logger.info("BriefingEngine initialized")
2586
+ except Exception as exc:
2587
+ logger.warning("BriefingEngine init failed: %s", exc)
2588
+
2589
+ # --- 10. World model ---
2590
+ world_store = None
2591
+ try:
2592
+ from apsimo.world_model.store import WorldModelStore
2593
+ from apsimo.world_model.config import WorldModelConfig
2594
+ _wm_backend = os.environ.get("WORLD_MODEL_BACKEND", "sqlite")
2595
+ world_store = WorldModelStore(WorldModelConfig(backend=_wm_backend))
2596
+ await world_store.connect()
2597
+ set_world_store(world_store)
2598
+ logger.info("WorldModelStore initialized and connected (backend=%s)", _wm_backend)
2599
+
2600
+ # World-model population from conversation (shadow-first). Boundary-checked
2601
+ # via the directive manager. Mode from COLONY_WORLD_POPULATE_MODE
2602
+ # (off|shadow|live, default shadow).
2603
+ try:
2604
+ from apsimo.world_model.populator import WorldModelPopulator, populate_mode
2605
+ from apsimo.api.routers.host import (
2606
+ get_directive_manager as _get_dm, set_world_populator,
2607
+ )
2608
+ _populator = WorldModelPopulator(world_store, directive_manager=_get_dm())
2609
+ set_world_populator(_populator)
2610
+ logger.info("WorldModelPopulator initialized (mode=%s)", populate_mode())
2611
+ except Exception as pexc:
2612
+ logger.warning("WorldModelPopulator init failed: %s", pexc)
2613
+
2614
+ # LLM-assisted world-model extraction (batch, journaled; daily phase).
2615
+ try:
2616
+ from apsimo.world_model.llm_extract import (
2617
+ WorldLLMExtractor, llm_extract_mode,
2618
+ )
2619
+ from apsimo.api.routers.host import (
2620
+ get_directive_manager as _get_dm3, set_world_llm_extractor, get_llm_router,
2621
+ )
2622
+ _wle = WorldLLMExtractor(
2623
+ world_store, graph=graph, directive_manager=_get_dm3(),
2624
+ journal=getattr(_sm_for_directed, "journal", None),
2625
+ self_model=_sm_for_directed,
2626
+ source_ledger=get_turn_idempotency_ledger(state_dir), router_provider=get_llm_router)
2627
+ set_world_llm_extractor(_wle)
2628
+ logger.info("WorldLLMExtractor initialized (mode=%s)",
2629
+ llm_extract_mode())
2630
+ except Exception as wexc:
2631
+ logger.warning("WorldLLMExtractor init failed: %s", wexc)
2632
+
2633
+ # Belief maintenance (item 7): contradiction detection, resolution,
2634
+ # stale decay + the inline property-supersession audit hook.
2635
+ try:
2636
+ from apsimo.beliefs import BeliefEngine, BeliefStore, beliefs_mode
2637
+ from apsimo.api.routers.host import set_belief_engine
2638
+ from apsimo.world_model.store import set_property_audit_hook
2639
+ _belief_store = BeliefStore(
2640
+ db_path=str(state_dir / "colony-beliefs.db"))
2641
+ _belief_eng = BeliefEngine(
2642
+ _belief_store, world_store=world_store, graph=graph,
2643
+ initiative_store=None, # attached below once wired
2644
+ journal=getattr(_sm_for_directed, "journal", None),
2645
+ self_model=_sm_for_directed)
2646
+ set_belief_engine(_belief_eng)
2647
+ set_property_audit_hook(_belief_eng.note_property_update)
2648
+ logger.info("BeliefEngine initialized (db=%s, mode=%s)",
2649
+ state_dir / "colony-beliefs.db", beliefs_mode())
2650
+ except Exception as bexc:
2651
+ logger.warning("BeliefEngine init failed: %s", bexc)
2652
+
2653
+ # Wire extraction pipeline
2654
+ try:
2655
+ from apsimo.world_model.extraction.pipeline import ExtractionPipeline
2656
+ from apsimo.world_model.extraction.formats import (
2657
+ TextExtractor, JSONExtractor, CSVExtractor,
2658
+ PDFExtractor, HTMLExtractor,
2659
+ )
2660
+ extractors = [TextExtractor(), JSONExtractor(), CSVExtractor()]
2661
+ if PDFExtractor:
2662
+ extractors.append(PDFExtractor())
2663
+ if HTMLExtractor:
2664
+ extractors.append(HTMLExtractor())
2665
+ llm_extract_fn = None
2666
+ if llm_router is not None:
2667
+ try:
2668
+ from apsimo.world_model.extraction.llm_extractor import (
2669
+ build_llm_extract_fn,
2670
+ )
2671
+ llm_extract_fn = build_llm_extract_fn(llm_router)
2672
+ except Exception as llm_exc:
2673
+ logger.warning("LLM extraction fallback disabled: %s", llm_exc)
2674
+
2675
+ pipeline = ExtractionPipeline(
2676
+ extractors=extractors,
2677
+ llm_extract_fn=llm_extract_fn,
2678
+ )
2679
+ set_extraction_pipeline(pipeline)
2680
+ logger.info(
2681
+ "Extraction pipeline initialized (%d format extractors, llm_fallback=%s)",
2682
+ len(extractors),
2683
+ "on" if llm_extract_fn is not None else "off",
2684
+ )
2685
+ except Exception as eexc:
2686
+ logger.warning("Extraction pipeline init skipped: %s", eexc)
2687
+ except Exception as exc:
2688
+ logger.warning("WorldModelStore init failed: %s", exc)
2689
+ # Try without connect() — some operations work without it
2690
+ try:
2691
+ world_store = WorldModelStore(WorldModelConfig(backend=_wm_backend))
2692
+ set_world_store(world_store)
2693
+ logger.info("WorldModelStore initialized (without connect)")
2694
+ except Exception as exc2:
2695
+ logger.error("WorldModelStore fallback init also failed: %s", exc2)
2696
+
2697
+ # --- 11. Cognition (CognitionPipeline) ---
2698
+ cognition_pipeline = None
2699
+ try:
2700
+ from apsimo.intelligence.cognition.registry import CognitionPipeline
2701
+ from apsimo.events.bus import EventBus
2702
+
2703
+ if graph is not None:
2704
+ # Create EventBus for real-time metrics
2705
+ event_bus = EventBus()
2706
+
2707
+ cognition_pipeline = CognitionPipeline(
2708
+ graph=graph,
2709
+ event_bus=event_bus,
2710
+ params=_adaptive_params,
2711
+ )
2712
+ _wire_controlled_learning_pipeline(
2713
+ cognition_pipeline, _controlled_learning)
2714
+ set_metalearner(cognition_pipeline.meta_learner)
2715
+ logger.info(
2716
+ "CognitionPipeline initialized (controlled proposals=%s, "
2717
+ "durable corrections=%s)",
2718
+ _controlled_learning.get("experiments") is not None,
2719
+ _controlled_learning.get("corrections") is not None,
2720
+ )
2721
+ else:
2722
+ logger.warning("CognitionPipeline skipped — ColonyGraph not available")
2723
+ except Exception as exc:
2724
+ logger.warning("CognitionPipeline init failed: %s", exc, exc_info=True)
2725
+
2726
+ # --- 12. Research pipeline ---
2727
+ try:
2728
+ from apsimo.research.search.orchestrator import SearchOrchestrator
2729
+
2730
+ # Wire search orchestrator
2731
+ search_orchestrator = SearchOrchestrator()
2732
+ search_provider = os.environ.get("COLONY_SEARCH_PROVIDER", "")
2733
+ if search_provider == "tavily" and os.environ.get("TAVILY_API_KEY"):
2734
+ from apsimo.research.search.tavily import TavilyProvider
2735
+ search_orchestrator.add_provider(TavilyProvider(os.environ["TAVILY_API_KEY"]))
2736
+ logger.info("Search provider: Tavily")
2737
+ elif search_provider == "serpapi" and os.environ.get("SERPAPI_KEY"):
2738
+ from apsimo.research.search.serpapi import SerpAPIProvider
2739
+ search_orchestrator.add_provider(SerpAPIProvider(os.environ["SERPAPI_KEY"]))
2740
+ logger.info("Search provider: SerpAPI")
2741
+ elif search_provider == "brave" and os.environ.get("BRAVE_API_KEY"):
2742
+ from apsimo.research.search.brave import BraveSearchProvider
2743
+ search_orchestrator.add_provider(BraveSearchProvider(os.environ["BRAVE_API_KEY"]))
2744
+ logger.info("Search provider: Brave")
2745
+ else:
2746
+ # Zero-config fallback so web_search works out of the box.
2747
+ from apsimo.research.search.duckduckgo import DuckDuckGoProvider
2748
+ search_orchestrator.add_provider(DuckDuckGoProvider())
2749
+ logger.info("Search provider: DuckDuckGo (default fallback)")
2750
+
2751
+ set_search_orchestrator(search_orchestrator)
2752
+
2753
+ # Register native tools with the ToolExecutor
2754
+ try:
2755
+ import apsimo.api.routers.host as _host_router
2756
+ te = _host_router._tool_executor
2757
+ except Exception:
2758
+ te = None
2759
+ if te is not None:
2760
+ sandbox_dir = os.environ.get("COLONY_SANDBOX_DIR", str(state_dir / "sandbox"))
2761
+ # Ensure the sandbox directory exists so file_ops don't fail on first call.
2762
+ Path(sandbox_dir).mkdir(parents=True, exist_ok=True)
2763
+ te.register_native_tools(
2764
+ search_orchestrator=search_orchestrator,
2765
+ sandbox_dir=sandbox_dir,
2766
+ )
2767
+ logger.info(
2768
+ "Native tools registered (calculate, web_search, file_ops; sandbox=%s)",
2769
+ sandbox_dir,
2770
+ )
2771
+
2772
+ research = _build_research_pipeline(
2773
+ graph=graph, p8_runtime=_p8_wiring)
2774
+ set_research_pipeline(research)
2775
+ logger.info("ResearchPipeline initialized")
2776
+ except Exception as exc:
2777
+ logger.warning("ResearchPipeline init failed: %s", exc, exc_info=True)
2778
+
2779
+ # --- 13. Delivery bridge ---
2780
+ try:
2781
+ from apsimo.delivery.bridge import ProactiveDeliveryBridge
2782
+ from apsimo.delivery.channels import ChannelRegistry
2783
+ channel_registry = ChannelRegistry.load(contacts_store=contacts_store, channel_store=channel_store)
2784
+ delivery = ProactiveDeliveryBridge(channel_registry=channel_registry)
2785
+ set_delivery_bridge(delivery)
2786
+ logger.info("Delivery bridge initialized")
2787
+
2788
+ # Adaptive daily cap (Amendment 1.6): the trust engine can EARN the
2789
+ # per-recipient cap upward with a proven delivery track record.
2790
+ try:
2791
+ _trust_for_cap = getattr(_sm_for_directed, "trust", None)
2792
+ if (_trust_for_cap is not None
2793
+ and getattr(delivery, "_rate_limiter", None) is not None):
2794
+ delivery._rate_limiter._cap_provider = _trust_for_cap.delivery_cap
2795
+ logger.info("Delivery rate cap wired to trust engine "
2796
+ "(adaptive, max=%s)",
2797
+ os.environ.get("COLONY_TRUST_DELIVERY_CAP_MAX", "6"))
2798
+ except Exception:
2799
+ logger.debug("adaptive cap wiring failed", exc_info=True)
2800
+
2801
+ # --- 13b. Briefings: full wiring (delivery + persistence + schedule) ---
2802
+ # The bare engine from section 9 can compose briefings but has no gateway, no
2803
+ # persistent store, and no scheduler -- proactive output never reaches anyone.
2804
+ # Rebuild it here, now that the delivery bridge exists: the bridge-backed
2805
+ # gateway auto-registers when a home channel is configured, the store persists
2806
+ # under COLONY_STATE_DIR, and the scheduler fires daily/weekly briefings and
2807
+ # drains pending deliveries. All deployment specifics come from env.
2808
+ try:
2809
+ from pathlib import Path as _P
2810
+
2811
+ from apsimo.briefings.config import BriefingConfig
2812
+ from apsimo.briefings.engine import BriefingEngine
2813
+ from apsimo.briefings.scheduler import BriefingScheduler
2814
+ from apsimo.briefings.store import BriefingStore
2815
+
2816
+ b_cfg = BriefingConfig()
2817
+ b_cfg.daily.time = os.environ.get("COLONY_BRIEFING_DAILY_TIME", b_cfg.daily.time)
2818
+ b_cfg.daily.timezone = os.environ.get("COLONY_BRIEFING_TZ", b_cfg.daily.timezone)
2819
+ b_cfg.weekly.timezone = b_cfg.daily.timezone
2820
+ b_cfg.delivery_gateway = os.environ.get("COLONY_BRIEFING_GATEWAY", "whatsapp")
2821
+ b_cfg.lm_enhancement_enabled = (
2822
+ os.environ.get("COLONY_BRIEFING_LM_ENHANCE", "0") not in ("0", "false", "no")
2823
+ )
2824
+ _b_state_dir = os.environ.get("COLONY_STATE_DIR", ".")
2825
+ b_store = BriefingStore(db_path=str(_P(_b_state_dir) / "briefings.db"))
2826
+ # Real aggregators where the backing subsystem exists — without
2827
+ # them the composer silently falls back to stubs and every data
2828
+ # section of every briefing is empty. Calendar/anomaly/mind/
2829
+ # synthesis still lack concrete aggregators (see docs/KNOWN-GAPS.md).
2830
+ _aggs = {}
2831
+ try:
2832
+ if graph is not None:
2833
+ from apsimo.briefings.aggregators import RelationshipAggregator
2834
+ _aggs["relationship_aggregator"] = RelationshipAggregator(
2835
+ scorer=None, graph=graph)
2836
+ except Exception:
2837
+ logger.debug("relationship aggregator wiring failed", exc_info=True)
2838
+ try:
2839
+ if goals_engine is not None:
2840
+ from apsimo.briefings.aggregators import GoalEngineAggregator
2841
+ _aggs["goal_aggregator"] = GoalEngineAggregator(goals_engine)
2842
+ except Exception:
2843
+ logger.debug("goal aggregator wiring failed", exc_info=True)
2844
+ try:
2845
+ # Both resolve their subsystems lazily off host globals at
2846
+ # call time (the anomaly detector doesn't even exist until
2847
+ # the autonomy registry builds it, well after this point).
2848
+ from apsimo.briefings.aggregators import (
2849
+ AnomalyDetectorAggregator, DiscovererSynthesisAggregator)
2850
+ _aggs["anomaly_aggregator"] = AnomalyDetectorAggregator()
2851
+ _aggs["synthesis_aggregator"] = DiscovererSynthesisAggregator()
2852
+ except Exception:
2853
+ logger.debug("anomaly/synthesis aggregator wiring failed", exc_info=True)
2854
+ try:
2855
+ # resolves the enabled calendar connector instance(s) — base
2856
+ # or per-account — at call time; harmless when none enabled
2857
+ from apsimo.briefings.aggregators import ConnectorCalendarAggregator
2858
+ _aggs["calendar_aggregator"] = ConnectorCalendarAggregator()
2859
+ except Exception:
2860
+ logger.debug("calendar aggregator wiring failed", exc_info=True)
2861
+ briefings = BriefingEngine(config=b_cfg, store=b_store,
2862
+ delivery_bridge=delivery, **_aggs)
2863
+ set_briefings_engine(briefings)
2864
+ if os.environ.get("COLONY_BRIEFINGS_SCHEDULE", "1") not in ("0", "false", "no"):
2865
+ b_sched = BriefingScheduler(config=b_cfg, engine=briefings, store=b_store)
2866
+ briefings.attach_scheduler(b_sched)
2867
+ b_sched.start()
2868
+ logger.info(
2869
+ "BriefingEngine rewired: gateway=%s daily=%s %s scheduler=on",
2870
+ b_cfg.delivery_gateway, b_cfg.daily.time, b_cfg.daily.timezone,
2871
+ )
2872
+ except Exception as exc:
2873
+ logger.warning("Briefing delivery wiring failed: %s", exc)
2874
+ except Exception as exc:
2875
+ logger.warning("ProactiveDeliveryBridge init failed: %s", exc)
2876
+
2877
+ # --- 14. Synthesis (ConnectionDiscoverer) ---
2878
+ try:
2879
+ from apsimo.intelligence.synthesis.connection_discoverer import ConnectionDiscoverer
2880
+ if graph is not None:
2881
+ discoverer = ConnectionDiscoverer(graph_client=graph)
2882
+ set_connection_discoverer(discoverer)
2883
+ logger.info("ConnectionDiscoverer initialized")
2884
+ else:
2885
+ logger.warning("ConnectionDiscoverer skipped — ColonyGraph not available")
2886
+ except Exception as exc:
2887
+ logger.warning("ConnectionDiscoverer init failed: %s", exc)
2888
+
2889
+ # Insight overlay store (tracks dismissed-insight IDs).
2890
+ try:
2891
+ from apsimo.intelligence.synthesis.insight_store import InsightStore
2892
+ insight_store = InsightStore(state_dir / "insights.db")
2893
+ set_insight_store(insight_store)
2894
+ logger.info("InsightStore initialized")
2895
+ except Exception as exc:
2896
+ logger.warning("InsightStore init failed: %s", exc)
2897
+
2898
+ # --- 15. Continuous learner ---
2899
+ try:
2900
+ from apsimo.intelligence.learning.continuous_learner import ContinuousLearner
2901
+ learner = ContinuousLearner()
2902
+ set_learner(learner)
2903
+ logger.info("ContinuousLearner initialized")
2904
+ except Exception as exc:
2905
+ logger.warning("ContinuousLearner init failed: %s", exc)
2906
+
2907
+ # --- 16. Skills registry + executor ---
2908
+ skills_registry = None
2909
+ try:
2910
+ from apsimo.skills.registry import SkillRegistry
2911
+ skills_registry = SkillRegistry()
2912
+ set_skills_registry(skills_registry)
2913
+ logger.info("SkillRegistry initialized (%d skills)", len(skills_registry.list_skills()))
2914
+
2915
+ try:
2916
+ from apsimo.skills.executor import SkillExecutor
2917
+ from apsimo.skills.security.guards import CapabilityGuard
2918
+ from apsimo.skills.security.scanner import ASTScanner
2919
+ skill_executor = SkillExecutor(
2920
+ registry=skills_registry,
2921
+ guard=CapabilityGuard(),
2922
+ scanner=ASTScanner(),
2923
+ )
2924
+ set_skill_executor(skill_executor)
2925
+ logger.info("SkillExecutor initialized")
2926
+ except Exception as sexc:
2927
+ logger.warning("SkillExecutor init failed: %s", sexc)
2928
+ except Exception as exc:
2929
+ logger.warning("SkillRegistry init failed: %s", exc)
2930
+
2931
+ # --- 17. Chain / Identity ---
2932
+ try:
2933
+ from apsimo.chain.identity import (
2934
+ get_or_create_colony_id,
2935
+ load_genesis_manifest,
2936
+ resolve_genesis_manifest_path,
2937
+ )
2938
+ colony_id = get_or_create_colony_id(state_dir)
2939
+
2940
+ # Load the Genesis manifest: this colony's own if it claimed Genesis,
2941
+ # otherwise the bundled network trust anchor.
2942
+ genesis_path = resolve_genesis_manifest_path(state_dir)
2943
+ if genesis_path is None:
2944
+ logger.warning("No Genesis manifest found; trust anchor stays unverified")
2945
+ else:
2946
+ load_genesis_manifest(genesis_path)
2947
+
2948
+ from apsimo.chain.manager import ChainManager
2949
+ chain = ChainManager(
2950
+ db_path=state_dir / "chain.db",
2951
+ colony_id=colony_id,
2952
+ )
2953
+ set_chain_manager(chain)
2954
+ logger.info("ChainManager initialized (colony_id=%s)", colony_id)
2955
+
2956
+ # Wire local key manager
2957
+ try:
2958
+ from apsimo.chain.local_keys import LocalKeyManager
2959
+ keys_dir = state_dir / "colony-keys"
2960
+ key_passphrase = os.environ.get("COLONY_KEY_PASSPHRASE", "")
2961
+ passphrase = key_passphrase.encode() if key_passphrase else None
2962
+
2963
+ if (keys_dir / "private.pem").exists():
2964
+ key_mgr = LocalKeyManager(keys_dir=keys_dir, colony_id=colony_id, passphrase=passphrase)
2965
+ logger.info("LocalKeyManager loaded (public_key=%s...)", key_mgr.public_key_hex()[:16])
2966
+ else:
2967
+ key_mgr = LocalKeyManager.generate(keys_dir=keys_dir, colony_id=colony_id, passphrase=passphrase)
2968
+ logger.info("LocalKeyManager generated new keypair for colony %s", colony_id)
2969
+
2970
+ chain._key_manager = key_mgr # Attach to chain for access
2971
+ except Exception as kexc:
2972
+ logger.warning("LocalKeyManager init skipped: %s", kexc)
2973
+
2974
+ # Initialize node identity
2975
+ try:
2976
+ from apsimo.chain.node import get_or_create_node_id, ensure_node_keypair, create_node_certificate
2977
+ node_id = get_or_create_node_id(state_dir)
2978
+ node_km = ensure_node_keypair(state_dir)
2979
+ logger.info("Node identity: %s (public_key=%s...)", node_id, node_km.public_key_hex()[:16])
2980
+
2981
+ # Create node certificate if missing
2982
+ cert_path = Path(state_dir) / "node-cert.json"
2983
+ if not cert_path.exists():
2984
+ create_node_certificate(state_dir, colony_key_manager=key_mgr)
2985
+ logger.info("Node certificate created and signed by Colony key")
2986
+ else:
2987
+ logger.info("Node certificate exists")
2988
+ except Exception as nexc:
2989
+ logger.warning("Node identity init skipped: %s", nexc)
2990
+
2991
+ # The LOCAL identity anchor (colony_id, node keypair, signed node
2992
+ # cert) is supported. The REMOTE multi-agent surface (agent connect,
2993
+ # cert-chain verification, block/consensus) is EXPERIMENTAL: no
2994
+ # consensus loop runs and the remote handshake is not production
2995
+ # verified. See docs/MULTI_AGENT.md.
2996
+ logger.info("Chain: local identity anchor ready; remote multi-agent "
2997
+ "+ consensus are EXPERIMENTAL (no consensus loop started)")
2998
+ except Exception as exc:
2999
+ logger.warning("ChainManager init failed: %s", exc)
3000
+
3001
+ # --- 18. Secrets ---
3002
+ try:
3003
+ from apsimo.secrets.manager import SecretsManager
3004
+ secrets = SecretsManager()
3005
+ set_secrets_manager(secrets)
3006
+ logger.info("SecretsManager initialized")
3007
+ except Exception as exc:
3008
+ logger.warning("SecretsManager init failed: %s", exc)
3009
+
3010
+ # --- 19. Session store ---
3011
+ try:
3012
+ from apsimo.sessions.store import InMemorySessionStore
3013
+ session_store = InMemorySessionStore()
3014
+ set_session_store(session_store)
3015
+ logger.info("InMemorySessionStore initialized")
3016
+
3017
+ # Re-wire ResponseGate now that session store is available
3018
+ if _gate_ref is not None:
3019
+ from apsimo.gate import ResponseGate
3020
+ new_gate = ResponseGate(_gate_config, session_store=session_store, audit_log=_gate_audit)
3021
+ set_response_gate(new_gate)
3022
+ logger.info("ResponseGate re-wired with SessionStore")
3023
+ except Exception as exc:
3024
+ logger.warning("SessionStore init failed: %s", exc)
3025
+
3026
+ # --- 20. Task queue ---
3027
+ task_queue = None
3028
+ try:
3029
+ from apsimo.task_queue.queue_manager import TaskQueueManager
3030
+ task_queue = await TaskQueueManager.initialize(
3031
+ db_path=state_dir / "task_queue.db",
3032
+ )
3033
+ # The queue facade becomes process-visible below, before the project
3034
+ # verifier and ProjectStore are constructed. Keep persisted
3035
+ # cognition WorkOrders fail-closed throughout that startup window;
3036
+ # successful project wiring replaces this with the digest-bound
3037
+ # callback later.
3038
+ _install_cognition_work_order_startup_fence(task_queue)
3039
+ task_queue.queue.set_execution_ready(False, "scheduler_starting")
3040
+ set_task_queue(task_queue)
3041
+ logger.info("TaskQueueManager initialized")
3042
+ except Exception as exc:
3043
+ logger.warning("TaskQueueManager init failed: %s", exc)
3044
+
3045
+ # The embedded worker starts only after the central governor is installed.
3046
+ worker_task = None
3047
+ queue_scheduler = None
3048
+ queue_scheduler_task = None
3049
+ queue_execution_ready = False
3050
+ agent_bridge_service = None
3051
+ agent_bridge_task = None
3052
+
3053
+ # --- 20c. Multi-Agent System (v0.7.0) ---
3054
+ try:
3055
+ from apsimo.agents.store import AgentStore, InviteStore
3056
+ from apsimo.initiatives.store import InitiativeStore
3057
+ from apsimo.initiatives.assignment import AssignmentEngine
3058
+ from apsimo.agents.websocket import WebSocketManager
3059
+
3060
+ agent_store = AgentStore(state_dir=state_dir)
3061
+ invite_store = InviteStore(state_dir=state_dir)
3062
+ set_agent_store(agent_store)
3063
+ set_invite_store(invite_store)
3064
+ logger.info("AgentStore initialized (state_dir=%s)", state_dir)
3065
+
3066
+ initiative_store = InitiativeStore(state_dir=state_dir)
3067
+ set_initiative_store(initiative_store)
3068
+ logger.info("InitiativeStore initialized (state_dir=%s)", state_dir)
3069
+
3070
+ # Observation store (v0.16.0) — agent-reported domain snapshots
3071
+ try:
3072
+ from apsimo.observations.store import ObservationStore
3073
+ from apsimo.api.routers.observations import set_observation_store
3074
+ observation_store = ObservationStore(state_dir=state_dir)
3075
+ set_observation_store(observation_store)
3076
+ logger.info("ObservationStore initialized (state_dir=%s)", state_dir)
3077
+ except Exception as exc:
3078
+ logger.warning("ObservationStore init failed (non-fatal): %s", exc)
3079
+
3080
+ assignment_engine = AssignmentEngine(
3081
+ agent_store=agent_store,
3082
+ initiative_store=initiative_store,
3083
+ )
3084
+ set_assignment_engine(assignment_engine)
3085
+ logger.info("AssignmentEngine initialized")
3086
+
3087
+ websocket_manager = WebSocketManager(
3088
+ agent_store=agent_store,
3089
+ initiative_store=initiative_store,
3090
+ )
3091
+ set_websocket_manager(websocket_manager)
3092
+ logger.info("WebSocketManager initialized")
3093
+ except Exception as exc:
3094
+ logger.warning("Multi-Agent System init failed: %s", exc)
3095
+
3096
+ # --- 21. Autonomy loop ---
3097
+ autonomy_config = None
3098
+ registry = None
3099
+ scheduler = None
3100
+ autonomy_loop = None
3101
+ try:
3102
+ from apsimo.autonomy.loop import AutonomyLoop
3103
+ from apsimo.autonomy.config import AutonomyConfig
3104
+ from apsimo.autonomy.registry import SubsystemRegistry
3105
+ from apsimo.autonomy.scheduler import AutonomyScheduler
3106
+ autonomy_config = AutonomyConfig.from_env()
3107
+
3108
+ # H4.2 annunciation: when the loop mode came from the preset (via
3109
+ # COLONY_PRESET_LOOP_COUPLING, default on) rather than an explicit
3110
+ # COLONY_AUTONOMY_MODE, say so loudly at startup and leave ONE
3111
+ # durable journal record the first time a deployment boots coupled —
3112
+ # a default flip an operator can always see and always roll back.
3113
+ if getattr(autonomy_config, "mode_source", "") == "preset":
3114
+ try:
3115
+ from apsimo.util.autonomy_preset import preset_name
3116
+ _preset = preset_name() or "(unknown)"
3117
+ except Exception:
3118
+ _preset = "(unknown)"
3119
+ logger.warning(
3120
+ "Autonomy loop mode %s inherited from COLONY_AUTONOMY_PRESET=%s "
3121
+ "via preset-loop coupling (COLONY_PRESET_LOOP_COUPLING=on by "
3122
+ "default). Set COLONY_AUTONOMY_MODE explicitly to override, or "
3123
+ "COLONY_PRESET_LOOP_COUPLING=off to restore env-only resolution.",
3124
+ autonomy_config.mode.value, _preset)
3125
+ try:
3126
+ from apsimo.api.routers.host import _self_model as _sm_j
3127
+ _cj = getattr(_sm_j, "journal", None)
3128
+ if _cj is not None and not _cj.recent(
3129
+ limit=1, domain="preset_coupling"):
3130
+ _cj.record(
3131
+ "preset_coupling",
3132
+ f"First coupled boot: loop mode {autonomy_config.mode.value} "
3133
+ f"inherited from preset '{_preset}'",
3134
+ reasoning="COLONY_AUTONOMY_MODE unset; "
3135
+ "COLONY_PRESET_LOOP_COUPLING on (default). "
3136
+ "Rollback: set COLONY_AUTONOMY_MODE=reactive "
3137
+ "or COLONY_PRESET_LOOP_COUPLING=off.",
3138
+ reversibility="reversible", decision="noted",
3139
+ ref="preset-loop-coupling")
3140
+ except Exception:
3141
+ pass # annunciation must never block startup
3142
+
3143
+ registry = SubsystemRegistry()
3144
+
3145
+ # Wire scheduler BEFORE the loop so the loop gets a direct reference.
3146
+ scheduler = AutonomyScheduler(db_path=str(state_dir / "schedules.db"))
3147
+ set_scheduler(scheduler)
3148
+ logger.info("AutonomyScheduler initialized")
3149
+
3150
+ autonomy_loop = AutonomyLoop(
3151
+ registry=registry,
3152
+ config=autonomy_config,
3153
+ scheduler=scheduler,
3154
+ )
3155
+ set_autonomy_loop(autonomy_loop)
3156
+
3157
+ # Register default periodic tasks. Every registered task does REAL
3158
+ # work or reports skipped — a no-op lambda returning {"status":"ok"}
3159
+ # makes the loop count a subsystem as running when it never does
3160
+ # (signal ingest happens inline at the API; briefings are fired by
3161
+ # the BriefingScheduler wired in section 13b — neither needs a task
3162
+ # here, so neither gets a fake one).
3163
+ def _run_health_check():
3164
+ return _scheduler_health_check(autonomy_loop)
3165
+
3166
+ scheduler.register("health_check", _run_health_check, interval_seconds=300, metadata={"description": "Subsystem health check (reports wired count)"})
3167
+
3168
+ async def _run_memory_consolidate():
3169
+ from apsimo.api.routers.host import _consolidator as c
3170
+ if c is None:
3171
+ return {"status": "skipped", "reason": "consolidator_not_wired"}
3172
+ result = await c.run()
3173
+ # ConsolidationResult exposes pairs_merged, not merged_count — the
3174
+ # old attr name silently reported merged:0 every run.
3175
+ return {"status": "ok", "merged": getattr(result, "pairs_merged", 0)}
3176
+
3177
+ scheduler.register("memory_consolidate", _run_memory_consolidate, interval_seconds=3600, metadata={"description": "Deduplicate and merge near-duplicate memories"})
3178
+
3179
+ async def _run_cpi_track():
3180
+ from apsimo.api.routers.host import _metalearner as ml
3181
+ if ml is None:
3182
+ return {"status": "skipped", "reason": "metalearner_not_wired"}
3183
+ cpi = await ml.evaluate()
3184
+ return {"status": "ok", "overall": round(float(getattr(cpi, "overall", 0.0)), 4)}
3185
+
3186
+ scheduler.register("cpi_track", _run_cpi_track, interval_seconds=86400, metadata={"description": "Calculate Cognitive Performance Index"})
3187
+
3188
+ async def _run_world_model_prune():
3189
+ from apsimo.api.routers.host import _world_store as ws
3190
+ if ws is None:
3191
+ return {"status": "skipped", "reason": "world_model_not_wired"}
3192
+ return await ws.prune()
3193
+
3194
+ scheduler.register("world_model_prune", _run_world_model_prune, interval_seconds=86400, metadata={"description": "Remove stale low-confidence world model entities (config TTL)"})
3195
+
3196
+ async def _run_mining_prune():
3197
+ from apsimo.api.routers.mining import get_mining_store
3198
+ store = get_mining_store()
3199
+ if store is None:
3200
+ return {"status": "skipped", "reason": "mining_not_wired"}
3201
+ try:
3202
+ retention_days = int(os.environ.get(
3203
+ "COLONY_MINING_RETENTION_DAYS", "0"))
3204
+ max_turns = int(os.environ.get(
3205
+ "COLONY_MINING_MAX_TURNS", "0"))
3206
+ except ValueError:
3207
+ retention_days = max_turns = 0
3208
+ if retention_days <= 0 and max_turns <= 0:
3209
+ # Default posture: the verbatim turn bank is unbounded
3210
+ # unless a deployment opts into retention.
3211
+ return {"status": "skipped", "reason": "retention_unbounded"}
3212
+ return {"status": "ok", **store.prune_turns(
3213
+ retention_days=retention_days, max_turns=max_turns)}
3214
+
3215
+ scheduler.register("mining_prune", _run_mining_prune, interval_seconds=86400, metadata={"description": "Prune banked mining turns per COLONY_MINING_RETENTION_DAYS / COLONY_MINING_MAX_TURNS (0 = keep everything)"})
3216
+
3217
+ # Daily pattern extraction (U25): the pattern store fed compute_surprise
3218
+ # but only ever filled via the manual /patterns/extract endpoint, so
3219
+ # surprise scoring ran against an empty store. Flag-gated off by
3220
+ # default; registered only when on so the loop never counts a
3221
+ # deliberately-disabled subsystem as running.
3222
+ if os.environ.get("COLONY_PATTERNS_SCHEDULE",
3223
+ "off").strip().lower() == "on":
3224
+ async def _run_pattern_extract():
3225
+ from apsimo.api.routers.host import (
3226
+ _pattern_store as ps, _world_store as ws,
3227
+ )
3228
+ if ws is None or ps is None:
3229
+ return {"status": "skipped",
3230
+ "reason": "stores_not_wired"}
3231
+ from apsimo.patterns.extract import extract_patterns
3232
+ return {"status": "ok",
3233
+ **extract_patterns(world_store=ws, pattern_store=ps)}
3234
+
3235
+ scheduler.register("pattern_extract", _run_pattern_extract, interval_seconds=86400, metadata={"description": "Extract world-model patterns into the pattern store (COLONY_PATTERNS_SCHEDULE=on)"})
3236
+
3237
+ async def _run_surprise_ttl():
3238
+ from apsimo.api.routers.host import _surprise_store as ss
3239
+ if ss is None:
3240
+ return {"status": "skipped",
3241
+ "reason": "surprise_store_not_wired"}
3242
+ try:
3243
+ ttl_days = float(os.environ.get(
3244
+ "COLONY_SURPRISE_TTL_DAYS", "14"))
3245
+ except ValueError:
3246
+ ttl_days = 14.0
3247
+ if ttl_days <= 0:
3248
+ return {"status": "skipped", "reason": "ttl_disabled"}
3249
+ return {"status": "ok", "auto_resolved": ss.resolve_stale(ttl_days)}
3250
+
3251
+ scheduler.register("surprise_ttl", _run_surprise_ttl, interval_seconds=86400, metadata={"description": "Auto-resolve surprises unaddressed past COLONY_SURPRISE_TTL_DAYS (default 14; 0 disables)"})
3252
+
3253
+ async def _run_digest_flush():
3254
+ from apsimo.api.routers.host import _delivery_bridge as bridge
3255
+ if bridge is None:
3256
+ return {"status": "skipped", "reason": "delivery_bridge_not_wired"}
3257
+ header = os.environ.get("COLONY_DIGEST_HEADER", "Daily digest")
3258
+ return await bridge.flush_digests_to_gateway(header=header)
3259
+
3260
+ digest_interval = int(os.environ.get("COLONY_DIGEST_INTERVAL_SECONDS", "86400"))
3261
+ scheduler.register(
3262
+ "digest_flush",
3263
+ _run_digest_flush,
3264
+ interval_seconds=digest_interval,
3265
+ metadata={"description": "Bundle and deliver accumulated DIGEST-channel items"},
3266
+ )
3267
+
3268
+ logger.info(
3269
+ "AutonomyLoop initialized (tick=%ds, scheduler=%d tasks)",
3270
+ autonomy_config.tick_interval_secs,
3271
+ len(scheduler.list_schedules()),
3272
+ )
3273
+
3274
+ # Auto-start the autonomy loop as a background task
3275
+ asyncio.create_task(autonomy_loop.start())
3276
+ logger.info("AutonomyLoop auto-start scheduled")
3277
+ except Exception as exc:
3278
+ logger.warning("AutonomyLoop init failed: %s", exc)
3279
+
3280
+ # Wire SubsystemRegistry into ToolExecutor so Colony-native tools
3281
+ # (memory_search, goals, relationships, etc.) are available to the
3282
+ # initiative executor's reasoning loop.
3283
+ if registry is not None and locals().get("tool_executor") is not None:
3284
+ te = locals()["tool_executor"]
3285
+ from apsimo.tools.handlers import TOOL_HANDLERS as _colony_handlers
3286
+ for _tname, _thandler in _colony_handlers.items():
3287
+ if _tname not in te._handlers:
3288
+ te._handlers[_tname] = lambda args, h=_thandler, r=registry: h(args, r)
3289
+ logger.info(
3290
+ "Colony tool handlers wired into ToolExecutor (%d colony tools, %d total)",
3291
+ len(_colony_handlers),
3292
+ len(te._handlers),
3293
+ )
3294
+
3295
+ # --- 22b. Project engine (goal persistence, cognition item 1) ---
3296
+ try:
3297
+ from apsimo.projects import ProjectEngine, ProjectStore, projects_mode
3298
+ from apsimo.work_orders import (
3299
+ QueueWorkOrderAdapter,
3300
+ load_receipt_verifier_from_env,
3301
+ )
3302
+ from apsimo.api.routers.host import (
3303
+ set_project_engine,
3304
+ get_directive_manager as _get_dm_p,
3305
+ _directed_service as _dsvc_for_projects,
3306
+ _proposal_store as _pstore_for_projects,
3307
+ _feedback_store as _fb_for_projects,
3308
+ )
3309
+
3310
+ async def _project_deliver(payload: dict) -> bool:
3311
+ try:
3312
+ from apsimo.api.routers.host import (
3313
+ _autonomy_loop, _delivery_bridge,
3314
+ )
3315
+ if _autonomy_loop is not None and _delivery_bridge is not None:
3316
+ return await _autonomy_loop._route_reachout_delivery(
3317
+ payload, _delivery_bridge)
3318
+ except Exception:
3319
+ logger.debug("project deliver failed", exc_info=True)
3320
+ return False
3321
+
3322
+ # A failed verifier import/configuration must not leave a project
3323
+ # engine from an earlier lifespan reachable in this process.
3324
+ set_project_engine(None)
3325
+ _receipt_verifier = load_receipt_verifier_from_env()
3326
+ _project_store = ProjectStore(db_path=str(state_dir / "colony-projects.db"))
3327
+ _project_concern_store = locals().get("_concern_store")
3328
+
3329
+ def _project_hold_reason(project) -> str:
3330
+ from apsimo.self_model.event_concerns import (
3331
+ project_turn_concern_hold_reason,
3332
+ )
3333
+ return project_turn_concern_hold_reason(
3334
+ project, _project_concern_store,
3335
+ )
3336
+
3337
+ _project_engine_obj = ProjectEngine(
3338
+ _project_store,
3339
+ directive_manager=_get_dm_p(),
3340
+ llm_router=llm_router,
3341
+ reasoning_loop=locals().get("reasoning_loop"),
3342
+ tool_executor=locals().get("tool_executor"),
3343
+ directed_service=_dsvc_for_projects,
3344
+ proposal_store=_pstore_for_projects,
3345
+ feedback_store=_fb_for_projects,
3346
+ self_model=_sm_for_directed,
3347
+ skill_store=_skills_mem_store,
3348
+ delivery_router=_project_deliver,
3349
+ initiative_store=locals().get("initiative_store"),
3350
+ work_order_adapter=(
3351
+ QueueWorkOrderAdapter(
3352
+ task_queue,
3353
+ project_store=_project_store,
3354
+ receipt_verifier=_receipt_verifier,
3355
+ )
3356
+ if task_queue is not None else None
3357
+ ),
3358
+ project_hold_reason=_project_hold_reason,
3359
+ )
3360
+ if task_queue is not None:
3361
+ _install_cognition_work_order_runtime_fence(
3362
+ task_queue, _project_store, _project_concern_store,
3363
+ )
3364
+ await task_queue.queue.reconcile_runtime_claim_holds()
3365
+ set_project_engine(_project_engine_obj)
3366
+ if _receipt_verifier is not None:
3367
+ logger.info(
3368
+ "External WorkOrder receipt verifier initialized (%s)",
3369
+ getattr(_receipt_verifier, "identity", type(_receipt_verifier).__name__),
3370
+ )
3371
+ logger.info("ProjectEngine initialized (db=%s, mode=%s)",
3372
+ state_dir / "colony-projects.db", projects_mode())
3373
+ # Late-attach the initiative store to the belief engine (it is wired
3374
+ # after the world-model section where the engine was created).
3375
+ try:
3376
+ from apsimo.api.routers.host import _belief_engine as _be
3377
+ if _be is not None:
3378
+ _be._initiatives = locals().get("initiative_store")
3379
+ except Exception:
3380
+ pass
3381
+ except Exception as exc:
3382
+ logger.warning("ProjectEngine init failed: %s", exc)
3383
+
3384
+ # --- 22b.1 Receipt-derived cognition evidence (migration-gated) ---
3385
+ _evidence_wiring = None
3386
+ try:
3387
+ _evidence_wiring = _attach_cognition_evidence(
3388
+ state_dir=state_dir,
3389
+ project_store=locals().get("_project_store"),
3390
+ self_model=_sm_for_directed,
3391
+ expectations=locals().get("_expectations"),
3392
+ scheduler=locals().get("scheduler"),
3393
+ )
3394
+ if _evidence_wiring is not None:
3395
+ logger.info(
3396
+ "Cognition evidence attached (mode=%s, db=%s, initial=%s)",
3397
+ _evidence_wiring["mode"],
3398
+ (
3399
+ state_dir / "colony-cognition-evidence.db"
3400
+ if _evidence_wiring["store"] is not None else "disabled"
3401
+ ),
3402
+ _evidence_wiring["initial_status"],
3403
+ )
3404
+ except Exception as exc:
3405
+ # A requested live pipeline suppresses the legacy direct competence
3406
+ # writer independently, so attachment failure loses no authority by
3407
+ # silently falling back to self-reported outcomes.
3408
+ logger.error("Cognition evidence attachment failed closed: %s", exc)
3409
+
3410
+ # --- 22b.2 Typed cognition/goal spine (P3, migration-gated) ---
3411
+ try:
3412
+ _cognition_spine = _attach_cognition_spine(
3413
+ state_dir=state_dir,
3414
+ task_queue=task_queue,
3415
+ workspace=locals().get("_workspace"),
3416
+ concern_store=locals().get("_concern_store"),
3417
+ project_store=locals().get("_project_store"),
3418
+ project_engine=locals().get("_project_engine_obj"),
3419
+ directive_manager=(
3420
+ _get_dm_p() if "_get_dm_p" in locals() else None
3421
+ ),
3422
+ llm_router=llm_router,
3423
+ embedded_worker_enabled=_configured_embedded_worker_enabled(),
3424
+ proposal_store=locals().get("_proposal_store"),
3425
+ )
3426
+ if _cognition_spine is not None:
3427
+ from apsimo.cognition.goal_spine import cognition_spine_mode
3428
+ logger.info(
3429
+ "Typed cognition spine attached (db=%s, mode=%s)",
3430
+ state_dir / "colony-cognition.db",
3431
+ cognition_spine_mode(),
3432
+ )
3433
+ except Exception as exc:
3434
+ # In live mode, P3's legacy-writer guards remain active and the
3435
+ # workspace loop refuses a missing spine. The failure is therefore
3436
+ # fail-closed for autonomous goal creation without taking the whole
3437
+ # sidecar (and daily owner-directed work) offline.
3438
+ from apsimo.cognition.goal_spine import cognition_spine_mode
3439
+ failed_mode = cognition_spine_mode()
3440
+ set_cognition_attachment_status({
3441
+ "configured_mode": failed_mode,
3442
+ "state": "failed",
3443
+ "reason": str(exc)[:500],
3444
+ "configured_handler_catalog": (
3445
+ ["thought"] if failed_mode in {"shadow", "live"} else []
3446
+ ),
3447
+ "effective_handler_catalog": [],
3448
+ })
3449
+ logger.error("Typed cognition spine attachment failed: %s", exc)
3450
+
3451
+ # --- 22b.3 Evidence-derived situation spine (P6, migration-gated) ---
3452
+ _situation_wiring = None
3453
+ try:
3454
+ _situation_wiring = _attach_situation_spine(
3455
+ state_dir=state_dir,
3456
+ cognition_spine=locals().get("_cognition_spine"),
3457
+ scheduler=locals().get("scheduler"),
3458
+ task_queue=task_queue,
3459
+ )
3460
+ if _situation_wiring is not None:
3461
+ logger.info(
3462
+ "Situation spine attached (db=%s, mode=%s, initial=%s)",
3463
+ state_dir / "colony-situation.db",
3464
+ _situation_wiring["mode"],
3465
+ _situation_wiring["initial_status"],
3466
+ )
3467
+ except Exception as exc:
3468
+ # A configured live P6 that cannot attach must not silently leave P3's
3469
+ # capacity-only allow path active. Preserve capacity denials, then hold
3470
+ # everything else until a healthy pinned restart.
3471
+ try:
3472
+ from apsimo.self_model.situation import situation_spine_mode
3473
+ cognition = locals().get("_cognition_spine")
3474
+ existing = getattr(cognition, "_situation", None)
3475
+ if situation_spine_mode() == "live" and callable(existing):
3476
+ cognition._situation = _capacity_plus_attachment_failure(
3477
+ existing, "situation_attachment_failed_closed",
3478
+ )
3479
+ except Exception:
3480
+ logger.exception("Could not install P6 attachment failure gate")
3481
+ logger.error("Situation spine attachment failed closed: %s", exc)
3482
+
3483
+ # --- 22b.4 Owner-ratified drive governance (P7, migration-gated) ---
3484
+ _drive_wiring = None
3485
+ try:
3486
+ _drive_wiring = _attach_drive_governance(
3487
+ state_dir=state_dir,
3488
+ cognition_spine=locals().get("_cognition_spine"),
3489
+ workspace=locals().get("_workspace"),
3490
+ project_store=locals().get("_project_store"),
3491
+ directive_manager=(
3492
+ _get_dm_p() if "_get_dm_p" in locals() else None
3493
+ ),
3494
+ approval_authority=_controlled_learning.get(
3495
+ "approval_authority"
3496
+ ),
3497
+ )
3498
+ if _drive_wiring is not None:
3499
+ logger.info(
3500
+ "Drive governance attached (db=%s, mode=%s, shared_authority=%s)",
3501
+ state_dir / "cognition-drive-governance.db",
3502
+ _drive_wiring["mode"],
3503
+ getattr(
3504
+ _drive_wiring["approval_authority"], "path", None,
3505
+ ),
3506
+ )
3507
+ except Exception as exc:
3508
+ logger.error("Drive governance attachment failed closed: %s", exc)
3509
+
3510
+ # --- 22c. Worker governor (server-side queue enforcement, item 5) ---
3511
+ try:
3512
+ from apsimo.api.routers.host import (
3513
+ get_directive_manager as _get_dm_w,
3514
+ _feedback_store as _fb_for_workers,
3515
+ )
3516
+ from apsimo.task_queue.governor import WorkerGovernor, workers_mode
3517
+
3518
+ async def _worker_deliver(payload: dict) -> bool:
3519
+ try:
3520
+ from apsimo.api.routers.host import (
3521
+ _autonomy_loop, _delivery_bridge,
3522
+ )
3523
+ if _autonomy_loop is not None and _delivery_bridge is not None:
3524
+ return await _autonomy_loop._route_reachout_delivery(
3525
+ payload, _delivery_bridge)
3526
+ except Exception:
3527
+ logger.debug("worker deliver failed", exc_info=True)
3528
+ return False
3529
+
3530
+ _worker_gov = WorkerGovernor(
3531
+ directive_manager=_get_dm_w(),
3532
+ feedback_store=_fb_for_workers,
3533
+ self_model=_sm_for_directed,
3534
+ delivery_router=_worker_deliver,
3535
+ proposal_store=locals().get("_proposal_store"),
3536
+ skill_store=_skills_mem_store,
3537
+ llm_router=llm_router,
3538
+ boundary_required=True,
3539
+ )
3540
+ set_worker_governor(_worker_gov)
3541
+ logger.info("WorkerGovernor initialized (mode=%s)", workers_mode())
3542
+ except Exception as exc:
3543
+ logger.warning("WorkerGovernor init failed: %s", exc)
3544
+
3545
+ # Queue maintenance is independent from execution-worker enablement. It
3546
+ # expires deadlines and leases, reconciles holds, and drains durable
3547
+ # outcome evidence even during a health-only deployment cutover.
3548
+ if task_queue is not None:
3549
+ try:
3550
+ from apsimo.task_queue.scheduler import Scheduler
3551
+
3552
+ queue_scheduler = Scheduler(
3553
+ queue=task_queue.queue,
3554
+ tick_interval_secs=_queue_seconds_env(
3555
+ "COLONY_QUEUE_SCHEDULER_TICK_SECS", 2.0,
3556
+ ),
3557
+ claim_timeout_secs=_queue_seconds_env(
3558
+ "COLONY_QUEUE_CLAIM_TIMEOUT_SECS", 30.0,
3559
+ ),
3560
+ readiness_callback=task_queue.queue.set_execution_ready,
3561
+ )
3562
+ # A successful synchronous first tick is the execution-readiness
3563
+ # gate. Configuration/import/database failures leave the API and
3564
+ # read surfaces online but no worker may claim work.
3565
+ await queue_scheduler.tick_once()
3566
+ queue_scheduler_task = asyncio.create_task(
3567
+ queue_scheduler.run()
3568
+ )
3569
+ app.state.queue_scheduler = queue_scheduler
3570
+ app.state.queue_scheduler_task = queue_scheduler_task
3571
+ queue_readiness = task_queue.queue.execution_readiness()
3572
+ queue_execution_ready = bool(queue_readiness.get("ready"))
3573
+ if queue_execution_ready:
3574
+ logger.info("Task queue scheduler started")
3575
+ else:
3576
+ logger.error(
3577
+ "Task queue maintenance started but execution remains "
3578
+ "held: %s",
3579
+ queue_readiness.get("reason"),
3580
+ )
3581
+ except Exception as exc:
3582
+ queue_execution_ready = False
3583
+ task_queue.queue.set_execution_ready(
3584
+ False, f"scheduler_unavailable:{exc}",
3585
+ )
3586
+ logger.error(
3587
+ "Task queue scheduler unavailable; execution workers disabled: %s",
3588
+ exc,
3589
+ exc_info=True,
3590
+ )
3591
+
3592
+ # --- 22c.0 Agent Bridge (after claim authority + scheduler readiness) ---
3593
+ # The bridge can still forward initiatives when queue execution is held,
3594
+ # but it receives no queue handle and therefore cannot claim jobs.
3595
+ try:
3596
+ from apsimo.services.agent_bridge import (
3597
+ create_from_env as _create_bridge,
3598
+ )
3599
+ agent_bridge_service = _create_bridge(
3600
+ initiative_store=locals().get("initiative_store"),
3601
+ autonomy_loop=autonomy_loop,
3602
+ task_queue=(task_queue if queue_execution_ready else None),
3603
+ observation_store=locals().get("observation_store"),
3604
+ )
3605
+ if agent_bridge_service is not None:
3606
+ set_agent_bridge(agent_bridge_service)
3607
+ agent_bridge_task = asyncio.create_task(
3608
+ agent_bridge_service.start()
3609
+ )
3610
+ logger.info("AgentBridgeService auto-start scheduled")
3611
+ except Exception as exc:
3612
+ logger.warning("AgentBridgeService init failed (non-fatal): %s", exc)
3613
+
3614
+ # --- 22c.1 Embedded worker (after mandatory central authority wiring) ---
3615
+ if task_queue is not None and _embedded_worker_enabled():
3616
+ try:
3617
+ if not queue_execution_ready:
3618
+ raise RuntimeError("queue scheduler is not execution-ready")
3619
+ import asyncio as _asyncio
3620
+ from apsimo.task_queue.worker import WorkerNode
3621
+ from apsimo.task_queue.handlers.registry import build_default_handlers
3622
+ from apsimo.chain.node import get_or_create_node_id
3623
+ import apsimo.api.routers.host as _host_mod
3624
+
3625
+ worker_node_id = get_or_create_node_id(state_dir)
3626
+ from apsimo.cognition.goal_spine import cognition_spine_mode
3627
+ worker_profile = _cognition_worker_profile(
3628
+ configured_mode=cognition_spine_mode(),
3629
+ attached=locals().get("_cognition_spine") is not None,
3630
+ )
3631
+ if worker_profile == "held":
3632
+ raise RuntimeError(
3633
+ "configured cognition owner is held because P3 attachment "
3634
+ "is unavailable"
3635
+ )
3636
+ if worker_profile == "thought_only":
3637
+ handlers, worker_capabilities = _cognition_owner_spec(
3638
+ router=_host_mod._llm_router,
3639
+ node_id=worker_node_id,
3640
+ )
3641
+ else:
3642
+ handlers = build_default_handlers(
3643
+ router=_host_mod._llm_router,
3644
+ world_model_store=_host_mod._world_store,
3645
+ contact_store=_host_mod._contacts_store,
3646
+ response_gate=_host_mod._response_gate,
3647
+ node_id=worker_node_id,
3648
+ )
3649
+ worker_capabilities = None
3650
+ worker = WorkerNode(
3651
+ node_id=worker_node_id,
3652
+ queue=task_queue.queue,
3653
+ handlers=handlers,
3654
+ capabilities=worker_capabilities,
3655
+ )
3656
+ worker_task = _asyncio.create_task(worker.start())
3657
+ app.state.worker = worker
3658
+ app.state.worker_task = worker_task
3659
+ if worker_profile == "thought_only":
3660
+ set_cognition_attachment_status({
3661
+ "configured_mode": cognition_spine_mode(),
3662
+ "state": "attached",
3663
+ "reason": "cognition_thought_worker_started",
3664
+ "configured_handler_catalog": ["thought"],
3665
+ "effective_handler_catalog": ["thought"],
3666
+ })
3667
+ logger.info(
3668
+ "WorkerNode started after central governor "
3669
+ "(node=%s, handlers=%s, governance=%s)",
3670
+ worker_node_id,
3671
+ [jt.value for jt in handlers.keys()],
3672
+ task_queue.queue.governance_configuration(),
3673
+ )
3674
+ except Exception as exc:
3675
+ logger.warning(
3676
+ "WorkerNode init failed — queued jobs will not execute: %s",
3677
+ exc,
3678
+ exc_info=True,
3679
+ )
3680
+
3681
+ # --- 22d. Exploration sandbox (gated isolated execution, item 6) ---
3682
+ try:
3683
+ from apsimo.sandbox import SandboxManager, sandbox_mode
3684
+ from apsimo.api.routers.host import (
3685
+ set_sandbox, get_directive_manager as _get_dm_sb,
3686
+ )
3687
+ _sandbox_mgr = SandboxManager(
3688
+ directive_manager=_get_dm_sb(),
3689
+ self_model=_sm_for_directed,
3690
+ )
3691
+ set_sandbox(_sandbox_mgr)
3692
+ logger.info("SandboxManager initialized (mode=%s, backend=%s)",
3693
+ sandbox_mode(), _sandbox_mgr.backend_name())
3694
+ except Exception as exc:
3695
+ logger.warning("SandboxManager init failed: %s", exc)
3696
+
3697
+ # --- 22e. Connector framework (read-only pull senses, item 2) ---
3698
+ try:
3699
+ from apsimo.connectors import (
3700
+ ConnectorManager, connectors_mode,
3701
+ )
3702
+ from apsimo.api.routers.host import (
3703
+ set_connector_manager, get_directive_manager as _get_dm_c,
3704
+ _world_populator as _pop_for_conn,
3705
+ )
3706
+ _conn_mgr = ConnectorManager(
3707
+ observation_store=locals().get("observation_store"),
3708
+ populator=_pop_for_conn,
3709
+ directive_manager=_get_dm_c(),
3710
+ self_model=_sm_for_directed,
3711
+ )
3712
+ n_conn = _conn_mgr.register_default_connectors()
3713
+ set_connector_manager(_conn_mgr)
3714
+ logger.info("ConnectorManager initialized (mode=%s, %d connector(s) enabled)",
3715
+ connectors_mode(), n_conn)
3716
+ except Exception as exc:
3717
+ logger.warning("ConnectorManager init failed: %s", exc)
3718
+
3719
+ # --- 23. Initiative Executor service (autonomous initiative processing) ---
3720
+ try:
3721
+ from apsimo.services.initiative_executor import (
3722
+ create_from_env as _create_executor,
3723
+ )
3724
+ from apsimo.api.routers.host import get_directive_manager as _get_dm
3725
+ _executor_svc = _create_executor(
3726
+ initiative_store=locals().get("initiative_store"),
3727
+ reasoning_loop=locals().get("reasoning_loop"),
3728
+ tool_executor=locals().get("tool_executor"),
3729
+ directive_manager=_get_dm(),
3730
+ skill_store=_skills_mem_store,
3731
+ self_model=_sm_for_directed,
3732
+ )
3733
+ if _executor_svc is not None:
3734
+ set_initiative_executor(_executor_svc)
3735
+ asyncio.create_task(_executor_svc.start())
3736
+ logger.info("InitiativeExecutorService auto-start scheduled")
3737
+ except Exception as exc:
3738
+ logger.warning("InitiativeExecutorService init failed (non-fatal): %s", exc)
3739
+
3740
+ from apsimo.telemetry import TelemetryStore
3741
+ telemetry = TelemetryStore()
3742
+ telemetry.load() # restore last_*_at across restart (v0.21.0)
3743
+ telemetry.started_at = datetime.now(timezone.utc)
3744
+ app.state.telemetry = telemetry
3745
+ set_telemetry(telemetry)
3746
+ logger.info("TelemetryStore initialized")
3747
+
3748
+ # Session report store (cross-session context bridge)
3749
+ from apsimo.sessions.reports import SessionReportStore
3750
+ session_report_store = SessionReportStore()
3751
+ set_session_report_store(session_report_store)
3752
+ logger.info("SessionReportStore initialized")
3753
+
3754
+ # Register conversation synthesis task (periodic memory scan for goals)
3755
+ try:
3756
+ if (
3757
+ autonomy_config is not None
3758
+ and getattr(autonomy_config, "conversation_synthesis_enabled", True)
3759
+ and registry is not None
3760
+ and scheduler is not None
3761
+ ):
3762
+ from apsimo.autonomy.synthesis import ConversationSynthesisTask
3763
+ _synthesis_task = ConversationSynthesisTask(
3764
+ registry=registry,
3765
+ lookback_hours=getattr(autonomy_config, "conversation_synthesis_lookback_hours", 2.0),
3766
+ min_confidence=getattr(autonomy_config, "conversation_synthesis_min_confidence", 0.35),
3767
+ telemetry=telemetry,
3768
+ )
3769
+ synthesis_interval = int(getattr(autonomy_config, "conversation_synthesis_interval_secs", 1800.0))
3770
+ scheduler.register(
3771
+ "conversation_synthesis",
3772
+ _synthesis_task.run,
3773
+ interval_seconds=synthesis_interval,
3774
+ metadata={"description": "Scan conversation memories for implicit goals and commitments"},
3775
+ )
3776
+ logger.info(
3777
+ "Conversation synthesis registered (lookback=%.1fh, interval=%ds, min_conf=%.2f)",
3778
+ getattr(autonomy_config, "conversation_synthesis_lookback_hours", 2.0),
3779
+ synthesis_interval,
3780
+ getattr(autonomy_config, "conversation_synthesis_min_confidence", 0.35),
3781
+ )
3782
+ except Exception as exc:
3783
+ logger.warning("Conversation synthesis registration failed: %s", exc)
3784
+
3785
+ logger.info("Sidecar capabilities: %s", supported_capabilities())
3786
+
3787
+ # Dedicated, owner-bound governed action execution. This ledger is
3788
+ # separate from the general API and task queue so a crash after an effect
3789
+ # starts can be recovered honestly as ambiguous without ever retrying the
3790
+ # mutation. No live action is enabled merely by constructing the service;
3791
+ # the exact scoped keyring principal is still required at the HTTP edge.
3792
+ governed_action_service = None
3793
+ try:
3794
+ import apsimo.api.routers.host as _host_actions
3795
+ from apsimo.api.routers.governed_actions import (
3796
+ set_governed_action_service,
3797
+ )
3798
+ from apsimo.governed_actions import (
3799
+ ColonySubsystemActionExecutor,
3800
+ GovernedActionLedger,
3801
+ GovernedActionService,
3802
+ )
3803
+
3804
+ async def _autonomy_enable():
3805
+ await _host_actions.autonomy_start()
3806
+
3807
+ async def _autonomy_disable():
3808
+ await _governed_autonomy_stop_signal(_host_actions._autonomy_loop)
3809
+
3810
+ def _autonomy_running():
3811
+ loop = _host_actions._autonomy_loop
3812
+ return bool(loop is not None and loop.is_running)
3813
+
3814
+ governed_action_service = GovernedActionService(
3815
+ GovernedActionLedger(
3816
+ state_dir / "governed-actions" / "ledger.db"
3817
+ ),
3818
+ ColonySubsystemActionExecutor(
3819
+ graph=_host_actions._graph,
3820
+ goals=_host_actions._goals_store,
3821
+ commitments=_host_actions._commitment_store,
3822
+ initiatives=_host_actions._initiative_store,
3823
+ projects=_host_actions._project_engine,
3824
+ feedback=_host_actions._feedback_store,
3825
+ autonomy_enable=_autonomy_enable,
3826
+ autonomy_disable=_autonomy_disable,
3827
+ autonomy_running=_autonomy_running,
3828
+ ),
3829
+ )
3830
+ set_governed_action_service(governed_action_service)
3831
+ logger.info("Governed action ledger initialized")
3832
+ except Exception as exc:
3833
+ logger.warning("Governed action ledger init failed: %s", exc)
3834
+ source_claim_task = None
3835
+ claims_enabled = os.environ.get("COLONY_SOURCE_CLAIMS", "on").strip().lower() in {"on", "1", "true"}
3836
+ from apsimo.vector import get_store as source_vector_store
3837
+ if claims_enabled or source_vector_store() is not None:
3838
+ from apsimo.turns import get_turn_idempotency_ledger
3839
+ from apsimo.beliefs.source_projection import run_source_claim_worker
3840
+ from apsimo.api.routers import host as source_claim_host
3841
+ source_claim_task = asyncio.create_task(run_source_claim_worker(
3842
+ get_turn_idempotency_ledger(state_dir), lambda: source_claim_host._llm_router,
3843
+ claims_enabled=claims_enabled))
3844
+ yield
3845
+
3846
+ if source_claim_task is not None:
3847
+ source_claim_task.cancel()
3848
+ try:
3849
+ await source_claim_task
3850
+ except asyncio.CancelledError:
3851
+ pass
3852
+
3853
+ # Shutdown — close connections
3854
+ set_external_event_intake(None)
3855
+ if _external_event_intake is not None:
3856
+ try:
3857
+ _external_event_intake.close()
3858
+ except Exception:
3859
+ logger.debug(
3860
+ "external cognition event intake shutdown failed",
3861
+ exc_info=True,
3862
+ )
3863
+ # Stop the queue-consuming bridge before its stores, governor, or queue.
3864
+ try:
3865
+ if agent_bridge_service is not None:
3866
+ await agent_bridge_service.stop()
3867
+ if agent_bridge_task is not None:
3868
+ try:
3869
+ await asyncio.wait_for(agent_bridge_task, timeout=5.0)
3870
+ except asyncio.TimeoutError:
3871
+ agent_bridge_task.cancel()
3872
+ try:
3873
+ await agent_bridge_task
3874
+ except asyncio.CancelledError:
3875
+ pass
3876
+ except Exception:
3877
+ logger.debug("Agent bridge shutdown error", exc_info=True)
3878
+ if graph is not None:
3879
+ try:
3880
+ await graph.close()
3881
+ except Exception:
3882
+ logger.debug("Graph close failed", exc_info=True)
3883
+ if world_store is not None:
3884
+ try:
3885
+ await world_store.close()
3886
+ except Exception:
3887
+ logger.debug("WorldStore close failed", exc_info=True)
3888
+ if skills_registry is not None:
3889
+ try:
3890
+ skills_registry.close()
3891
+ except Exception:
3892
+ logger.debug("SkillRegistry close failed", exc_info=True)
3893
+ set_llm_router(None)
3894
+ set_reasoning_loop(None)
3895
+ set_graph(None)
3896
+ set_response_gate(None)
3897
+ set_signal_collector(None)
3898
+ set_embedder(None)
3899
+ set_goals_engine(None)
3900
+ set_contacts_store(None)
3901
+ set_briefings_engine(None)
3902
+ set_world_store(None)
3903
+ set_metalearner(None)
3904
+ try:
3905
+ from apsimo.api.routers.host import (
3906
+ set_adaptive_params as _set_adaptive_params,
3907
+ set_benchmark as _set_benchmark,
3908
+ set_experiments as _set_experiments,
3909
+ set_learning_feedback_store as _set_learning_feedback_store,
3910
+ )
3911
+ _set_experiments(None)
3912
+ _set_benchmark(None)
3913
+ _set_learning_feedback_store(None)
3914
+ _set_adaptive_params(None)
3915
+ if _adaptive_params is not None:
3916
+ _adaptive_params.close()
3917
+ except Exception:
3918
+ logger.debug("controlled learning shutdown failed", exc_info=True)
3919
+ set_research_pipeline(None)
3920
+ set_delivery_bridge(None)
3921
+ set_connection_discoverer(None)
3922
+ set_learner(None)
3923
+ set_skills_registry(None)
3924
+ set_commitment_store(None)
3925
+ set_affect_store(None)
3926
+ try:
3927
+ if _p8_wiring is not None:
3928
+ _p8_wiring.close()
3929
+ except Exception:
3930
+ logger.debug("P8 runtime shutdown failed", exc_info=True)
3931
+ set_p8_runtime(None)
3932
+ set_facts_store(None)
3933
+ try:
3934
+ from apsimo.api.routers.host import (
3935
+ set_presence_store as _set_presence_store,
3936
+ _presence_store as _presence_ref,
3937
+ )
3938
+ if _presence_ref is not None:
3939
+ _presence_ref.close()
3940
+ _set_presence_store(None)
3941
+ except Exception:
3942
+ logger.debug("presence store shutdown failed", exc_info=True)
3943
+ set_context_provenance_store(None)
3944
+ set_response_guard(None)
3945
+ try:
3946
+ from apsimo.tom.levels import set_evidence_probe
3947
+ set_evidence_probe(None)
3948
+ except Exception:
3949
+ logger.debug("Tom2 evidence probe shutdown failed", exc_info=True)
3950
+ set_pattern_store(None)
3951
+ set_surprise_store(None)
3952
+ set_tom_extractor(None)
3953
+ if channel_store is not None:
3954
+ try:
3955
+ channel_store.close()
3956
+ except Exception:
3957
+ logger.debug("ChannelStore close failed", exc_info=True)
3958
+ try:
3959
+ from apsimo.channels.router import set_channel_store as _set_ch_store
3960
+ _set_ch_store(None)
3961
+ except Exception:
3962
+ pass
3963
+ set_chain_manager(None)
3964
+ set_secrets_manager(None)
3965
+ set_session_store(None)
3966
+ set_session_report_store(None)
3967
+ set_agent_bridge(None)
3968
+ set_initiative_executor(None)
3969
+ # Stop worker node (before queue so in-flight jobs can drain).
3970
+ try:
3971
+ worker = getattr(app.state, "worker", None)
3972
+ if worker is not None:
3973
+ await worker.stop(drain_timeout=10.0)
3974
+ worker_task = getattr(app.state, "worker_task", None)
3975
+ if worker_task is not None:
3976
+ worker_task.cancel()
3977
+ except Exception:
3978
+ logger.debug("Worker shutdown error", exc_info=True)
3979
+ # Stop queue maintenance only after the execution worker has drained, and
3980
+ # before closing the shared queue connection.
3981
+ try:
3982
+ if queue_scheduler is not None:
3983
+ await queue_scheduler.stop()
3984
+ if queue_scheduler_task is not None:
3985
+ try:
3986
+ await asyncio.wait_for(queue_scheduler_task, timeout=5.0)
3987
+ except asyncio.TimeoutError:
3988
+ queue_scheduler_task.cancel()
3989
+ try:
3990
+ await queue_scheduler_task
3991
+ except asyncio.CancelledError:
3992
+ pass
3993
+ except Exception:
3994
+ logger.debug("Task queue scheduler shutdown error", exc_info=True)
3995
+ # No stopped/repeated lifespan may retain a usable authority handle.
3996
+ set_worker_governor(None)
3997
+ # Stop task queue
3998
+ try:
3999
+ from apsimo.api.routers.host import _task_queue
4000
+ if _task_queue is not None:
4001
+ await _task_queue.queue.stop()
4002
+ except Exception:
4003
+ logger.warning("Task queue shutdown failed")
4004
+ set_task_queue(None)
4005
+ # Stop autonomy loop if running
4006
+ try:
4007
+ from apsimo.api.routers.host import _autonomy_loop
4008
+ if _autonomy_loop is not None and _autonomy_loop.is_running:
4009
+ await _autonomy_loop.stop()
4010
+ except Exception:
4011
+ logger.warning("Autonomy loop shutdown failed")
4012
+ set_autonomy_loop(None)
4013
+ # Evidence/P6 periodic reducers are owned by the autonomy scheduler. Close
4014
+ # them only after that loop is stopped so an in-flight tick cannot race a
4015
+ # closed SQLite handle. Clear HTTP handles before releasing connections.
4016
+ set_cognition_evidence(None, None, None, {
4017
+ "configured_mode": "off",
4018
+ "state": "off",
4019
+ "reason": "sidecar_stopped",
4020
+ })
4021
+ try:
4022
+ evidence_wiring = locals().get("_evidence_wiring")
4023
+ if evidence_wiring is not None and evidence_wiring.get("store") is not None:
4024
+ evidence_wiring["store"].close()
4025
+ except Exception:
4026
+ logger.debug("cognition evidence shutdown failed", exc_info=True)
4027
+ set_situation_spine(None, None)
4028
+ set_drive_governance(None, None, None)
4029
+ try:
4030
+ situation_wiring = locals().get("_situation_wiring")
4031
+ if situation_wiring is not None:
4032
+ original = situation_wiring.get("original_validator")
4033
+ cognition = locals().get("_cognition_spine")
4034
+ if original is not None and cognition is not None:
4035
+ cognition._situation = original
4036
+ situation_wiring["store"].close()
4037
+ except Exception:
4038
+ logger.debug("situation spine shutdown failed", exc_info=True)
4039
+ try:
4040
+ drive_wiring = locals().get("_drive_wiring")
4041
+ if drive_wiring is not None:
4042
+ drive_wiring["store"].close()
4043
+ except Exception:
4044
+ logger.debug("drive governance shutdown failed", exc_info=True)
4045
+ try:
4046
+ from apsimo.api.routers.host import set_project_engine
4047
+ set_project_engine(None)
4048
+ project_store = locals().get("_project_store")
4049
+ if project_store is not None:
4050
+ project_store.close()
4051
+ except Exception:
4052
+ logger.debug("project store shutdown failed", exc_info=True)
4053
+ set_session_store(None)
4054
+ set_task_queue(None)
4055
+ # Multi-Agent cleanup
4056
+ set_agent_store(None)
4057
+ set_invite_store(None)
4058
+ set_initiative_store(None)
4059
+ try:
4060
+ from apsimo.api.routers.governed_actions import (
4061
+ set_governed_action_service,
4062
+ )
4063
+ set_governed_action_service(None)
4064
+ if governed_action_service is not None:
4065
+ governed_action_service.close()
4066
+ except Exception:
4067
+ logger.debug("Governed action ledger shutdown failed", exc_info=True)
4068
+ set_assignment_engine(None)
4069
+ set_websocket_manager(None)
4070
+ try:
4071
+ from apsimo.api.routers.observations import set_observation_store
4072
+ set_observation_store(None)
4073
+ except Exception:
4074
+ pass
4075
+ auth_telemetry = getattr(app.state, "auth_telemetry", None)
4076
+ if auth_telemetry is not None:
4077
+ try:
4078
+ auth_telemetry.close()
4079
+ except Exception:
4080
+ logger.debug("Auth telemetry shutdown failed", exc_info=True)
4081
+ logger.info("Sidecar shutdown complete")
4082
+
4083
+
4084
+ def create_app() -> FastAPI:
4085
+ """Build and return the FastAPI application."""
4086
+ app = FastAPI(
4087
+ title="Apsimo",
4088
+ version="0.1.0",
4089
+ lifespan=lifespan,
4090
+ )
4091
+
4092
+ # API authentication (skips health/docs; loopback dev mode if no auth set).
4093
+ # The scoped keyring runs alongside COLONY_API_KEY during migration.
4094
+ from apsimo.api.auth_telemetry import AuthTelemetry
4095
+ from apsimo.api.contact_grants import ContactGrantRegistry
4096
+ from apsimo.api.middleware import ApiKeyMiddleware, BodySizeLimitMiddleware
4097
+
4098
+ # Body-size cap runs before auth so oversized payloads are rejected with
4099
+ # 413 regardless of the auth state.
4100
+ try:
4101
+ max_body = int(os.environ.get("COLONY_MAX_BODY_BYTES", "") or 10 * 1024 * 1024)
4102
+ except ValueError:
4103
+ max_body = 10 * 1024 * 1024
4104
+ app.add_middleware(BodySizeLimitMiddleware, max_bytes=max_body)
4105
+
4106
+ api_key = os.environ.get("COLONY_API_KEY")
4107
+ keyring_path = os.environ.get("COLONY_API_KEYRING_PATH")
4108
+ state_dir = _state_dir()
4109
+ telemetry_path = os.environ.get(
4110
+ "COLONY_AUTH_TELEMETRY_PATH",
4111
+ str(state_dir / "colony-auth-telemetry.db"),
4112
+ )
4113
+ contact_grants_path = os.environ.get(
4114
+ "COLONY_API_CONTACT_GRANTS_PATH",
4115
+ str(state_dir / "api-contact-grants.json"),
4116
+ )
4117
+ auth_telemetry = AuthTelemetry(telemetry_path)
4118
+ contact_grants = ContactGrantRegistry(contact_grants_path)
4119
+ # HTTP middleware uses request.state; WebSocket/admin status paths use
4120
+ # app.state because BaseHTTPMiddleware does not wrap WebSocket frames.
4121
+ app.state.auth_telemetry = auth_telemetry
4122
+ app.state.contact_grants = contact_grants
4123
+ app.add_middleware(
4124
+ ApiKeyMiddleware,
4125
+ api_key=api_key,
4126
+ keyring_path=keyring_path,
4127
+ auth_telemetry=auth_telemetry,
4128
+ contact_grants=contact_grants,
4129
+ )
4130
+ if api_key and keyring_path:
4131
+ logger.info("Legacy and scoped API authentication enabled (dual-accept)")
4132
+ elif keyring_path:
4133
+ logger.info("Scoped API authentication enabled")
4134
+ elif api_key:
4135
+ logger.info("Legacy API key authentication enabled")
4136
+ else:
4137
+ logger.warning("No API auth configured — API is open (loopback dev mode)")
4138
+
4139
+ app.include_router(host_router)
4140
+ app.include_router(host_v2_router)
4141
+ from apsimo.api.routers import executions as executions_router
4142
+ app.include_router(executions_router.router)
4143
+ from apsimo.api.routers import commitment_work as commitment_work_router
4144
+ app.include_router(commitment_work_router.router)
4145
+ from apsimo.api.routers import initiative_work as initiative_work_router
4146
+ app.include_router(initiative_work_router.router)
4147
+ from apsimo.api.routers import social_state as social_state_router
4148
+ app.include_router(social_state_router.router)
4149
+ from apsimo.api.routers import temporal_followups as temporal_followups_router
4150
+ app.include_router(temporal_followups_router.router)
4151
+ from apsimo.api.routers import transport as transport_router
4152
+ app.include_router(transport_router.router)
4153
+ from apsimo.api.routers import followup_plans
4154
+ app.include_router(followup_plans.router)
4155
+
4156
+ # Exact PUT/GET action endpoint. Middleware maps these methods to
4157
+ # actions:execute/actions:verify; the router independently rejects legacy,
4158
+ # unscoped, non-owner, and differently named principals.
4159
+ from apsimo.api.routers import governed_actions as governed_actions_router
4160
+ app.include_router(governed_actions_router.router)
4161
+
4162
+ # Channel registration router
4163
+ from apsimo.channels.router import router as channels_router
4164
+ app.include_router(channels_router)
4165
+
4166
+ # Task queue router (v0.13.0)
4167
+ from apsimo.api.routers import task_queue as task_queue_router
4168
+ app.include_router(task_queue_router.router)
4169
+
4170
+ # Observations router (v0.16.0) — agent-as-sensor ingestion
4171
+ from apsimo.api.routers import observations as observations_router
4172
+ app.include_router(observations_router.router)
4173
+ from apsimo.api.routers import mining as mining_router
4174
+ app.include_router(mining_router.router)
4175
+
4176
+ # Context gate (v0.32.0) — budget-aware context preparation
4177
+ from apsimo.api.routers import context_gate as context_gate_router
4178
+ app.include_router(context_gate_router.router)
4179
+
4180
+ # MCP streamable HTTP endpoint
4181
+ try:
4182
+ from apsimo.mcp.server import create_server
4183
+ mcp_server = create_server()
4184
+ # Mount MCP ASGI app at /mcp
4185
+ mcp_asgi = mcp_server.streamable_http_app()
4186
+ app.mount("/mcp", mcp_asgi)
4187
+ logger.info("MCP endpoint mounted at /mcp (streamable HTTP)")
4188
+ except ImportError:
4189
+ logger.debug("MCP SDK not installed — /mcp endpoint not available (install colonyai[mcp])")
4190
+ except Exception as exc:
4191
+ logger.warning("Could not mount MCP endpoint: %s", exc)
4192
+
4193
+ return app
4194
+
4195
+
4196
+ # Uvicorn entry point
4197
+ app = create_app()